@shahmilsaari/memory-core 1.0.13 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -36
- package/dist/{chunk-PRRVI3YM.js → chunk-UNGXRKD2.js} +75 -21
- package/dist/cli.js +744 -147
- package/dist/dashboard/assets/index-Bz-Tzypa.css +1 -0
- package/dist/dashboard/assets/index-CaevtejN.js +2 -0
- package/dist/dashboard/index.html +6 -3
- package/dist/{dashboard-server-53HVL7LF.js → dashboard-server-ZBGR4CO7.js} +6 -4
- package/package.json +1 -1
- package/dist/dashboard/assets/index-CJyZEmIe.css +0 -1
- package/dist/dashboard/assets/index-DM82nOf5.js +0 -2
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-UNGXRKD2.js";
|
|
24
24
|
|
|
25
25
|
// src/cli.ts
|
|
26
26
|
import { Command } from "commander";
|
|
@@ -33,7 +33,7 @@ import { homedir } from "os";
|
|
|
33
33
|
|
|
34
34
|
// src/hook.ts
|
|
35
35
|
import { execSync, spawnSync } from "child_process";
|
|
36
|
-
import { writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync, readFileSync as readFileSync2, chmodSync } from "fs";
|
|
36
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync, readFileSync as readFileSync2, chmodSync, statSync } from "fs";
|
|
37
37
|
import { join as join2 } from "path";
|
|
38
38
|
import chalk from "chalk";
|
|
39
39
|
|
|
@@ -122,8 +122,14 @@ 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
|
-
|
|
125
|
+
var COMMIT_MSG_HOOK_PATH = join2(".git", "hooks", "commit-msg");
|
|
126
|
+
var COMMIT_MSG_HOOK_MARKER = "# archmind-memory-core commit-msg";
|
|
127
|
+
var RULE_CACHE_FILE = ".memory-core-rules-cache.json";
|
|
128
|
+
var DB_VERSION_FILE = ".memory-core-db-version";
|
|
129
|
+
var RULE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
130
|
+
function buildHookBody(advisory, fast = false) {
|
|
126
131
|
const suffix = advisory ? " || true" : "";
|
|
132
|
+
const checkArgs = fast ? "check --staged --fast" : "check --staged";
|
|
127
133
|
return `${HOOK_MARKER}${advisory ? " advisory" : ""}
|
|
128
134
|
if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
|
|
129
135
|
exit 0
|
|
@@ -135,20 +141,20 @@ if [ -n "\${SKIP_HOOKS:-}" ]; then
|
|
|
135
141
|
exit 0
|
|
136
142
|
fi
|
|
137
143
|
if command -v memory-core >/dev/null 2>&1; then
|
|
138
|
-
memory-core
|
|
144
|
+
memory-core ${checkArgs}${suffix}
|
|
139
145
|
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
140
|
-
./node_modules/.bin/memory-core
|
|
146
|
+
./node_modules/.bin/memory-core ${checkArgs}${suffix}
|
|
141
147
|
elif [ -f "./dist/cli.js" ]; then
|
|
142
|
-
node ./dist/cli.js
|
|
148
|
+
node ./dist/cli.js ${checkArgs}${suffix}
|
|
143
149
|
else
|
|
144
|
-
npx --no-install memory-core
|
|
150
|
+
npx --no-install memory-core ${checkArgs} 2>/dev/null || exit 0
|
|
145
151
|
fi
|
|
146
152
|
`;
|
|
147
153
|
}
|
|
148
|
-
function buildHookScript(advisory) {
|
|
154
|
+
function buildHookScript(advisory, fast = false) {
|
|
149
155
|
return `#!/bin/sh
|
|
150
156
|
|
|
151
|
-
${buildHookBody(advisory)}`;
|
|
157
|
+
${buildHookBody(advisory, fast)}`;
|
|
152
158
|
}
|
|
153
159
|
function normalizeHookPreamble(content) {
|
|
154
160
|
const lines = content.split("\n");
|
|
@@ -165,6 +171,31 @@ function normalizeHookPreamble(content) {
|
|
|
165
171
|
}
|
|
166
172
|
return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
167
173
|
}
|
|
174
|
+
function toRuleStatEntry(raw) {
|
|
175
|
+
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
176
|
+
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
177
|
+
return raw;
|
|
178
|
+
}
|
|
179
|
+
function readPositiveIntEnv(name, fallback) {
|
|
180
|
+
const raw = Number(process.env[name]);
|
|
181
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
|
|
182
|
+
}
|
|
183
|
+
function isFastCheck(options) {
|
|
184
|
+
return options.fast === true || process.env.MEMORY_CORE_CHECK_FAST === "1";
|
|
185
|
+
}
|
|
186
|
+
async function withTimeout(promise, timeoutMs, fallback) {
|
|
187
|
+
let timer;
|
|
188
|
+
try {
|
|
189
|
+
return await Promise.race([
|
|
190
|
+
promise,
|
|
191
|
+
new Promise((resolve2) => {
|
|
192
|
+
timer = setTimeout(() => resolve2(fallback), timeoutMs);
|
|
193
|
+
})
|
|
194
|
+
]);
|
|
195
|
+
} finally {
|
|
196
|
+
if (timer) clearTimeout(timer);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
168
199
|
function recordViolations(violations, source = "hook") {
|
|
169
200
|
const statsPath = join2(process.cwd(), ".memory-core-stats.json");
|
|
170
201
|
let stats = { rules: {}, files: {} };
|
|
@@ -178,7 +209,8 @@ function recordViolations(violations, source = "hook") {
|
|
|
178
209
|
stats.rules ??= {};
|
|
179
210
|
stats.files ??= {};
|
|
180
211
|
for (const violation of violations) {
|
|
181
|
-
|
|
212
|
+
const existing = toRuleStatEntry(stats.rules[violation.rule]);
|
|
213
|
+
stats.rules[violation.rule] = { count: existing.count + 1, falsePositives: existing.falsePositives };
|
|
182
214
|
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
183
215
|
}
|
|
184
216
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -219,11 +251,12 @@ async function promptToSaveViolations(violations) {
|
|
|
219
251
|
message: "Why should this rule exist?",
|
|
220
252
|
default: selected.reason ?? selected.issue ?? ""
|
|
221
253
|
});
|
|
254
|
+
const storedReason = reason.trim() || selected.reason || selected.issue || `Captured from violation: ${selected.rule}`;
|
|
222
255
|
await app.services.memoryEngine.remember({
|
|
223
256
|
type: "rule",
|
|
224
257
|
scope: "project",
|
|
225
258
|
content: selected.rule,
|
|
226
|
-
reason:
|
|
259
|
+
reason: storedReason,
|
|
227
260
|
tags: ["violation"]
|
|
228
261
|
});
|
|
229
262
|
console.log(chalk.green(" \u2713 Saved as project rule. Run memory-core sync to propagate it.\n"));
|
|
@@ -232,6 +265,40 @@ async function promptToSaveViolations(violations) {
|
|
|
232
265
|
`));
|
|
233
266
|
}
|
|
234
267
|
}
|
|
268
|
+
function readRuleCache(cwd) {
|
|
269
|
+
const cachePath = join2(cwd, RULE_CACHE_FILE);
|
|
270
|
+
const configPath = join2(cwd, ".memory-core.json");
|
|
271
|
+
if (!existsSync2(cachePath) || !existsSync2(configPath)) return null;
|
|
272
|
+
try {
|
|
273
|
+
const entry = JSON.parse(readFileSync2(cachePath, "utf-8"));
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
if (now - entry.timestamp > RULE_CACHE_TTL_MS) return null;
|
|
276
|
+
const configMtime = statSync(configPath).mtimeMs;
|
|
277
|
+
if (configMtime !== entry.configMtime) return null;
|
|
278
|
+
const dbVersionPath = join2(cwd, DB_VERSION_FILE);
|
|
279
|
+
const dbVersionMtime = existsSync2(dbVersionPath) ? statSync(dbVersionPath).mtimeMs : 0;
|
|
280
|
+
if (dbVersionMtime !== entry.dbVersionMtime) return null;
|
|
281
|
+
return entry;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function saveRuleCache(cwd, data) {
|
|
287
|
+
const configPath = join2(cwd, ".memory-core.json");
|
|
288
|
+
try {
|
|
289
|
+
const configMtime = statSync(configPath).mtimeMs;
|
|
290
|
+
const dbVersionPath = join2(cwd, DB_VERSION_FILE);
|
|
291
|
+
const dbVersionMtime = existsSync2(dbVersionPath) ? statSync(dbVersionPath).mtimeMs : 0;
|
|
292
|
+
const entry = {
|
|
293
|
+
timestamp: Date.now(),
|
|
294
|
+
configMtime,
|
|
295
|
+
dbVersionMtime,
|
|
296
|
+
...data
|
|
297
|
+
};
|
|
298
|
+
writeFileSync2(join2(cwd, RULE_CACHE_FILE), JSON.stringify(entry, null, 2) + "\n", "utf-8");
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
}
|
|
235
302
|
async function loadIgnorePatterns() {
|
|
236
303
|
try {
|
|
237
304
|
const app = getDefaultApplicationContainer();
|
|
@@ -465,6 +532,28 @@ function loadRecentViolationsFromStats(cwd = process.cwd()) {
|
|
|
465
532
|
return [];
|
|
466
533
|
}
|
|
467
534
|
}
|
|
535
|
+
function incrementFalsePositivesForPatterns(learnedPatterns, violations, cwd = process.cwd()) {
|
|
536
|
+
if (learnedPatterns.length === 0 || violations.length === 0) return;
|
|
537
|
+
const statsPath = join2(cwd, ".memory-core-stats.json");
|
|
538
|
+
if (!existsSync2(statsPath)) return;
|
|
539
|
+
let stats;
|
|
540
|
+
try {
|
|
541
|
+
stats = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
542
|
+
} catch {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
stats.rules ??= {};
|
|
546
|
+
for (const violation of violations) {
|
|
547
|
+
const haystack = `${violation.rule}
|
|
548
|
+
${violation.issue}
|
|
549
|
+
${violation.file}`.toLowerCase();
|
|
550
|
+
const matched = learnedPatterns.some((p) => haystack.includes(p.toLowerCase()));
|
|
551
|
+
if (!matched) continue;
|
|
552
|
+
const existing = toRuleStatEntry(stats.rules[violation.rule]);
|
|
553
|
+
stats.rules[violation.rule] = { count: existing.count, falsePositives: existing.falsePositives + 1 };
|
|
554
|
+
}
|
|
555
|
+
writeFileSync2(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
556
|
+
}
|
|
468
557
|
async function learnGlobalIgnoresFromFalsePositives(options) {
|
|
469
558
|
if (options.currentViolations.length === 0) return [];
|
|
470
559
|
const recentViolations = loadRecentViolationsFromStats();
|
|
@@ -493,10 +582,11 @@ ${JSON.stringify(options.allowPatterns, null, 2)}`;
|
|
|
493
582
|
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
583
|
}
|
|
495
584
|
try {
|
|
585
|
+
const recheckTimeoutMs = readPositiveIntEnv("MEMORY_CORE_FALSE_POSITIVE_TIMEOUT_MS", 6e3);
|
|
496
586
|
const raw = await callChatModel([
|
|
497
587
|
{ role: "system", content: systemPrompt },
|
|
498
588
|
{ role: "user", content: userPrompt }
|
|
499
|
-
]);
|
|
589
|
+
], { timeoutMs: recheckTimeoutMs });
|
|
500
590
|
const parsed = parseFalsePositiveDecisions(raw);
|
|
501
591
|
if (!parsed.valid) return [];
|
|
502
592
|
const existing = new Set(options.allowPatterns.map((pattern) => pattern.toLowerCase()));
|
|
@@ -522,6 +612,9 @@ ${JSON.stringify(options.allowPatterns, null, 2)}`;
|
|
|
522
612
|
} catch {
|
|
523
613
|
}
|
|
524
614
|
}
|
|
615
|
+
if (inserted.length > 0) {
|
|
616
|
+
incrementFalsePositivesForPatterns(inserted, options.currentViolations);
|
|
617
|
+
}
|
|
525
618
|
return inserted;
|
|
526
619
|
} catch {
|
|
527
620
|
return [];
|
|
@@ -606,6 +699,32 @@ function findDeterministicViolations(diff, rules, avoids, allowPatterns = []) {
|
|
|
606
699
|
}
|
|
607
700
|
return dedupeViolations(violations);
|
|
608
701
|
}
|
|
702
|
+
function suppressBatchRepetitions(violations, threshold = 3) {
|
|
703
|
+
const pairCounts = /* @__PURE__ */ new Map();
|
|
704
|
+
for (const v of violations) {
|
|
705
|
+
const key = `${v.rule}\0${v.file}`;
|
|
706
|
+
pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
|
|
707
|
+
}
|
|
708
|
+
const suppressedKeys = /* @__PURE__ */ new Set();
|
|
709
|
+
for (const [key, count] of pairCounts) {
|
|
710
|
+
if (count >= threshold) suppressedKeys.add(key);
|
|
711
|
+
}
|
|
712
|
+
if (suppressedKeys.size === 0) return { filtered: violations, suppressedCount: 0 };
|
|
713
|
+
const filtered = violations.filter((v) => !suppressedKeys.has(`${v.rule}\0${v.file}`));
|
|
714
|
+
return { filtered, suppressedCount: violations.length - filtered.length };
|
|
715
|
+
}
|
|
716
|
+
function groupViolationsByRule(violations) {
|
|
717
|
+
const groups = /* @__PURE__ */ new Map();
|
|
718
|
+
for (const v of violations) {
|
|
719
|
+
const existing = groups.get(v.rule);
|
|
720
|
+
if (existing) {
|
|
721
|
+
existing.push(v);
|
|
722
|
+
} else {
|
|
723
|
+
groups.set(v.rule, [v]);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return groups;
|
|
727
|
+
}
|
|
609
728
|
function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
|
|
610
729
|
if (violations.length === 0) return violations;
|
|
611
730
|
const changedFiles = new Set(stagedFiles.map((file) => normalizePath(file)));
|
|
@@ -637,13 +756,13 @@ function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
|
|
|
637
756
|
}
|
|
638
757
|
return filtered;
|
|
639
758
|
}
|
|
640
|
-
function installHook(advisory = true) {
|
|
759
|
+
function installHook(advisory = true, fast = false) {
|
|
641
760
|
if (!existsSync2(".git")) {
|
|
642
761
|
console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
|
|
643
762
|
process.exit(1);
|
|
644
763
|
}
|
|
645
|
-
const script = buildHookScript(advisory);
|
|
646
|
-
const body = buildHookBody(advisory).trimEnd();
|
|
764
|
+
const script = buildHookScript(advisory, fast);
|
|
765
|
+
const body = buildHookBody(advisory, fast).trimEnd();
|
|
647
766
|
if (existsSync2(HOOK_PATH)) {
|
|
648
767
|
const existing = readFileSync2(HOOK_PATH, "utf-8");
|
|
649
768
|
if (existing.includes(HOOK_MARKER)) {
|
|
@@ -658,8 +777,10 @@ ${preamble}`;
|
|
|
658
777
|
${body}
|
|
659
778
|
`);
|
|
660
779
|
chmodSync(HOOK_PATH, 493);
|
|
780
|
+
installCommitMsgHook(advisory);
|
|
661
781
|
const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
|
|
662
782
|
console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
|
|
783
|
+
if (fast) console.log(chalk.gray(` Check mode: fast deterministic checks`));
|
|
663
784
|
return;
|
|
664
785
|
}
|
|
665
786
|
writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
|
|
@@ -667,9 +788,11 @@ ${body}
|
|
|
667
788
|
writeFileSync2(HOOK_PATH, script);
|
|
668
789
|
}
|
|
669
790
|
chmodSync(HOOK_PATH, 493);
|
|
791
|
+
installCommitMsgHook(advisory);
|
|
670
792
|
const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
|
|
671
793
|
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"}`));
|
|
794
|
+
console.log(chalk.gray(fast ? " Check mode: fast deterministic checks" : ` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
|
|
795
|
+
console.log(chalk.gray(" Commit message rules: memory-core commit-rules --list"));
|
|
673
796
|
console.log(chalk.gray(" To uninstall: memory-core hook uninstall\n"));
|
|
674
797
|
}
|
|
675
798
|
function uninstallHook() {
|
|
@@ -690,8 +813,118 @@ function uninstallHook() {
|
|
|
690
813
|
} else {
|
|
691
814
|
unlinkSync(HOOK_PATH);
|
|
692
815
|
}
|
|
816
|
+
uninstallCommitMsgHook();
|
|
693
817
|
console.log(chalk.green("\n \u2713 Pre-commit hook removed\n"));
|
|
694
818
|
}
|
|
819
|
+
function buildCommitMsgHookBody(advisory) {
|
|
820
|
+
const suffix = advisory ? " || true" : "";
|
|
821
|
+
return `${COMMIT_MSG_HOOK_MARKER}${advisory ? " advisory" : ""}
|
|
822
|
+
if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
|
|
823
|
+
exit 0
|
|
824
|
+
fi
|
|
825
|
+
if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
|
|
826
|
+
exit 0
|
|
827
|
+
fi
|
|
828
|
+
if [ -n "\${SKIP_HOOKS:-}" ]; then
|
|
829
|
+
exit 0
|
|
830
|
+
fi
|
|
831
|
+
if command -v memory-core >/dev/null 2>&1; then
|
|
832
|
+
memory-core check --commit-msg "$1"${suffix}
|
|
833
|
+
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
834
|
+
./node_modules/.bin/memory-core check --commit-msg "$1"${suffix}
|
|
835
|
+
elif [ -f "./dist/cli.js" ]; then
|
|
836
|
+
node ./dist/cli.js check --commit-msg "$1"${suffix}
|
|
837
|
+
else
|
|
838
|
+
npx --no-install memory-core check --commit-msg "$1" 2>/dev/null || exit 0
|
|
839
|
+
fi
|
|
840
|
+
`;
|
|
841
|
+
}
|
|
842
|
+
function installCommitMsgHook(advisory = true) {
|
|
843
|
+
const body = buildCommitMsgHookBody(advisory).trimEnd();
|
|
844
|
+
const script = `#!/bin/sh
|
|
845
|
+
|
|
846
|
+
${body}
|
|
847
|
+
`;
|
|
848
|
+
if (existsSync2(COMMIT_MSG_HOOK_PATH)) {
|
|
849
|
+
const existing = readFileSync2(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
850
|
+
if (existing.includes(COMMIT_MSG_HOOK_MARKER)) {
|
|
851
|
+
const markerIndex = existing.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
852
|
+
const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
|
|
853
|
+
const normalizedBefore = normalizeHookPreamble(beforeRaw);
|
|
854
|
+
const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
|
|
855
|
+
const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
|
|
856
|
+
${preamble}`;
|
|
857
|
+
writeFileSync2(COMMIT_MSG_HOOK_PATH, `${preambleWithShebang}
|
|
858
|
+
|
|
859
|
+
${body}
|
|
860
|
+
`);
|
|
861
|
+
} else {
|
|
862
|
+
writeFileSync2(COMMIT_MSG_HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
writeFileSync2(COMMIT_MSG_HOOK_PATH, script);
|
|
866
|
+
}
|
|
867
|
+
chmodSync(COMMIT_MSG_HOOK_PATH, 493);
|
|
868
|
+
}
|
|
869
|
+
function uninstallCommitMsgHook() {
|
|
870
|
+
if (!existsSync2(COMMIT_MSG_HOOK_PATH)) return;
|
|
871
|
+
const content = readFileSync2(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
872
|
+
if (!content.includes(COMMIT_MSG_HOOK_MARKER)) return;
|
|
873
|
+
const markerIndex = content.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
874
|
+
const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
|
|
875
|
+
if (before && before !== "#!/bin/sh") {
|
|
876
|
+
writeFileSync2(COMMIT_MSG_HOOK_PATH, `${before}
|
|
877
|
+
`);
|
|
878
|
+
} else {
|
|
879
|
+
unlinkSync(COMMIT_MSG_HOOK_PATH);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function checkCommitMsg(msgFile, options = {}) {
|
|
883
|
+
if (!existsSync2(msgFile)) {
|
|
884
|
+
if (options.verbose) console.log(chalk.gray(" No commit message file \u2014 skipping."));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const raw = readFileSync2(msgFile, "utf-8");
|
|
888
|
+
const cleanMsg = raw.split("\n").filter((l) => !l.startsWith("#")).join("\n").trim();
|
|
889
|
+
if (!cleanMsg) {
|
|
890
|
+
if (options.verbose) console.log(chalk.gray(" Empty commit message \u2014 skipping."));
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const configPath = join2(process.cwd(), ".memory-core.json");
|
|
894
|
+
if (!existsSync2(configPath)) return;
|
|
895
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
896
|
+
const rules = (config.commitRules ?? []).filter(Boolean);
|
|
897
|
+
if (rules.length === 0) return;
|
|
898
|
+
console.log(chalk.cyan("\n archmind \u2014 checking commit message\u2026"));
|
|
899
|
+
const violations = [];
|
|
900
|
+
for (const rule of rules) {
|
|
901
|
+
try {
|
|
902
|
+
const regex = new RegExp(rule.pattern, "im");
|
|
903
|
+
const matched = regex.test(cleanMsg);
|
|
904
|
+
const violated = rule.negate ? matched : !matched;
|
|
905
|
+
if (violated) violations.push({ rule });
|
|
906
|
+
} catch {
|
|
907
|
+
if (options.debug) console.log(chalk.yellow(` [debug] Invalid regex: "${rule.pattern}"`));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (violations.length === 0) {
|
|
911
|
+
console.log(chalk.green(" \u2713 Commit message OK.\n"));
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const blocking = violations.filter((v) => !v.rule.advisory);
|
|
915
|
+
violations.forEach(({ rule }) => {
|
|
916
|
+
const prefix = rule.advisory ? chalk.yellow(" \u26A0 ") : chalk.red(" \u2717 ");
|
|
917
|
+
console.log(prefix + rule.message);
|
|
918
|
+
const matchLabel = rule.negate ? "(must NOT match)" : "(must match)";
|
|
919
|
+
console.log(chalk.dim(` Pattern: ${rule.pattern} ${matchLabel}`));
|
|
920
|
+
});
|
|
921
|
+
console.log();
|
|
922
|
+
if (blocking.length === 0) return;
|
|
923
|
+
console.log(chalk.dim(" Fix the commit message, then commit again."));
|
|
924
|
+
console.log(chalk.dim(" To bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
|
|
925
|
+
console.log(chalk.dim(" Manage rules: memory-core commit-rules --list\n"));
|
|
926
|
+
process.exit(1);
|
|
927
|
+
}
|
|
695
928
|
async function checkStaged(options = {}) {
|
|
696
929
|
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
697
930
|
let diff;
|
|
@@ -720,17 +953,45 @@ async function checkStaged(options = {}) {
|
|
|
720
953
|
if (!existsSync2(configPath)) return;
|
|
721
954
|
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
722
955
|
const { rules: fallbackRules, avoids } = getProfileRules(config);
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
956
|
+
const fast = isFastCheck(options);
|
|
957
|
+
const ruleLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
|
|
958
|
+
const ignoreLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_IGNORE_LOAD_TIMEOUT_MS", 1500);
|
|
959
|
+
let rules;
|
|
960
|
+
let ignores;
|
|
961
|
+
let allowPatterns;
|
|
962
|
+
if (fast) {
|
|
963
|
+
rules = fallbackRules;
|
|
964
|
+
ignores = [];
|
|
965
|
+
allowPatterns = [...new Set(getAllowPatterns(config))];
|
|
966
|
+
} else {
|
|
967
|
+
const cwd = process.cwd();
|
|
968
|
+
const cached = readRuleCache(cwd);
|
|
969
|
+
if (cached) {
|
|
970
|
+
rules = cached.rules;
|
|
971
|
+
ignores = cached.ignores;
|
|
972
|
+
allowPatterns = cached.allowPatterns;
|
|
973
|
+
if (options.debug) {
|
|
974
|
+
console.log(chalk.gray(" [debug] using cached rules (TTL valid)"));
|
|
975
|
+
}
|
|
976
|
+
} else {
|
|
977
|
+
const [loadedRules, loadedIgnores] = await Promise.all([
|
|
978
|
+
withTimeout(loadRelevantRules(config, diff, stagedFiles, fallbackRules), ruleLoadTimeoutMs, fallbackRules),
|
|
979
|
+
withTimeout(loadIgnorePatterns(), ignoreLoadTimeoutMs, [])
|
|
980
|
+
]);
|
|
981
|
+
rules = loadedRules;
|
|
982
|
+
ignores = loadedIgnores;
|
|
983
|
+
allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...loadedIgnores])];
|
|
984
|
+
saveRuleCache(cwd, { rules, ignores, allowPatterns });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
728
987
|
if (rules.length === 0) return;
|
|
729
|
-
const
|
|
988
|
+
const modelInputMaxChars = readPositiveIntEnv("MEMORY_CORE_MODEL_INPUT_MAX_CHARS", 8e3);
|
|
989
|
+
const modelInput = buildModelInputFromDiff(diff, modelInputMaxChars);
|
|
730
990
|
console.log(chalk.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
|
|
731
991
|
if (options.verbose || options.debug) {
|
|
732
992
|
const sourceLabel = modelInput.source === "added-lines" ? "added lines" : "diff";
|
|
733
|
-
|
|
993
|
+
const modelLabel = fast ? "skipped (--fast)" : getChatProviderLabel();
|
|
994
|
+
console.log(chalk.gray(` model: ${modelLabel} rules: ${rules.length} diff: ${diff.length} chars input: ${sourceLabel}${modelInput.truncated ? " (truncated)" : ""}`));
|
|
734
995
|
}
|
|
735
996
|
const rulesWithReasons = rules.map((r, i) => {
|
|
736
997
|
const why = reasonMap.get(r);
|
|
@@ -773,13 +1034,19 @@ Do not include any text outside the JSON object.`;
|
|
|
773
1034
|
reasonLookup: reasonMap
|
|
774
1035
|
});
|
|
775
1036
|
let modelViolations = [];
|
|
776
|
-
|
|
1037
|
+
let aiFallback = fast;
|
|
1038
|
+
if (fast) {
|
|
1039
|
+
if (options.verbose || options.debug) {
|
|
1040
|
+
console.log(chalk.gray(" AI check skipped; running deterministic checks only."));
|
|
1041
|
+
}
|
|
1042
|
+
} else try {
|
|
1043
|
+
const checkTimeoutMs = readPositiveIntEnv("MEMORY_CORE_CHECK_TIMEOUT_MS", readPositiveIntEnv("CHAT_TIMEOUT_MS", 2e4));
|
|
777
1044
|
const raw = await callChatModel([
|
|
778
1045
|
{ role: "system", content: systemPrompt },
|
|
779
1046
|
{ role: "user", content: `Review these staged changes:
|
|
780
1047
|
|
|
781
1048
|
${modelInput.text}` }
|
|
782
|
-
]);
|
|
1049
|
+
], { timeoutMs: checkTimeoutMs });
|
|
783
1050
|
if (options.verbose || options.debug) {
|
|
784
1051
|
console.log(chalk.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
|
|
785
1052
|
}
|
|
@@ -792,15 +1059,25 @@ ${modelInput.text}` }
|
|
|
792
1059
|
} catch (err) {
|
|
793
1060
|
if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
|
|
794
1061
|
printModelMissing(err.message.split(":")[1]);
|
|
1062
|
+
aiFallback = true;
|
|
1063
|
+
modelViolations = [];
|
|
1064
|
+
} else if (err.message?.startsWith("TIMEOUT:")) {
|
|
1065
|
+
const timeoutMs = err.message.split(":")[1];
|
|
1066
|
+
console.log(chalk.yellow(`
|
|
1067
|
+
\u26A0 AI check timed out after ${timeoutMs}ms \u2014 switching to fast deterministic checks for this run.`));
|
|
1068
|
+
console.log(chalk.gray(" Set MEMORY_CORE_CHECK_TIMEOUT_MS to tune this.\n"));
|
|
1069
|
+
aiFallback = true;
|
|
795
1070
|
modelViolations = [];
|
|
796
1071
|
} else if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
797
1072
|
console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 using deterministic checks only."));
|
|
798
1073
|
console.log(chalk.gray(" Start it: ollama serve\n"));
|
|
1074
|
+
aiFallback = true;
|
|
799
1075
|
modelViolations = [];
|
|
800
1076
|
} else {
|
|
801
1077
|
console.log(chalk.yellow(`
|
|
802
1078
|
\u26A0 AI rule check failed: ${err.message}`));
|
|
803
1079
|
console.log(chalk.gray(" Using deterministic checks only.\n"));
|
|
1080
|
+
aiFallback = true;
|
|
804
1081
|
modelViolations = [];
|
|
805
1082
|
}
|
|
806
1083
|
}
|
|
@@ -808,6 +1085,17 @@ ${modelInput.text}` }
|
|
|
808
1085
|
let violations = dedupeViolations([...deterministicViolations, ...astViolations, ...modelViolations]);
|
|
809
1086
|
violations = applyAllowPatterns(violations, allowPatterns);
|
|
810
1087
|
if (violations.length > 0) {
|
|
1088
|
+
const { filtered, suppressedCount } = suppressBatchRepetitions(violations);
|
|
1089
|
+
if (suppressedCount > 0) {
|
|
1090
|
+
console.log(
|
|
1091
|
+
chalk.dim(
|
|
1092
|
+
` \u2139 Auto-suppressed ${suppressedCount} repetitive violation${suppressedCount > 1 ? "s" : ""} (same rule fired \u22653\xD7 on the same file \u2014 consider tuning the rule)`
|
|
1093
|
+
)
|
|
1094
|
+
);
|
|
1095
|
+
violations = filtered;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (!aiFallback && violations.length > 0) {
|
|
811
1099
|
const learnedPatterns = await learnGlobalIgnoresFromFalsePositives({
|
|
812
1100
|
diff,
|
|
813
1101
|
currentViolations: violations,
|
|
@@ -834,16 +1122,66 @@ ${modelInput.text}` }
|
|
|
834
1122
|
`
|
|
835
1123
|
)
|
|
836
1124
|
);
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1125
|
+
let ruleStatsSnapshot = {};
|
|
1126
|
+
{
|
|
1127
|
+
const statsPath = join2(process.cwd(), ".memory-core-stats.json");
|
|
1128
|
+
if (existsSync2(statsPath)) {
|
|
1129
|
+
try {
|
|
1130
|
+
const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
1131
|
+
ruleStatsSnapshot = parsed.rules ?? {};
|
|
1132
|
+
} catch {
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
const MAX_LOCATIONS = 5;
|
|
1137
|
+
const groups = groupViolationsByRule(violations);
|
|
1138
|
+
let groupIndex = 0;
|
|
1139
|
+
for (const [rule, group] of groups) {
|
|
1140
|
+
groupIndex++;
|
|
1141
|
+
const isCluster = group.length > 1;
|
|
1142
|
+
const first = group[0];
|
|
1143
|
+
if (isCluster) {
|
|
1144
|
+
console.log(chalk.bold.red(`
|
|
1145
|
+
[${groupIndex}] ${rule}`) + chalk.dim(` \xD7${group.length}`));
|
|
1146
|
+
} else {
|
|
1147
|
+
const loc = first.file ? first.line ? `${first.file}:${first.line}` : first.file : "unknown location";
|
|
1148
|
+
console.log(chalk.bold(`
|
|
1149
|
+
[${groupIndex}] ${loc}`));
|
|
1150
|
+
console.log(chalk.yellow(" Rule: ") + rule);
|
|
1151
|
+
}
|
|
1152
|
+
const why = first.reason ?? reasonMap.get(rule);
|
|
842
1153
|
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
843
|
-
if (
|
|
844
|
-
if (
|
|
1154
|
+
if (first.suggestion) console.log(chalk.green(" Fix: ") + first.suggestion);
|
|
1155
|
+
if (isCluster) {
|
|
1156
|
+
console.log();
|
|
1157
|
+
const shown = group.slice(0, MAX_LOCATIONS);
|
|
1158
|
+
const overflow = group.length - MAX_LOCATIONS;
|
|
1159
|
+
for (const v of shown) {
|
|
1160
|
+
const loc = v.file ? v.line ? `${v.file}:${v.line}` : v.file : "unknown location";
|
|
1161
|
+
const issue = v.issue ? chalk.dim(` ${v.issue}`) : "";
|
|
1162
|
+
console.log(chalk.dim(` ${loc}`) + issue);
|
|
1163
|
+
}
|
|
1164
|
+
if (overflow > 0) {
|
|
1165
|
+
console.log(chalk.dim(` ... and ${overflow} more`));
|
|
1166
|
+
}
|
|
1167
|
+
} else {
|
|
1168
|
+
if (first.issue) console.log(chalk.red(" Issue: ") + first.issue);
|
|
1169
|
+
}
|
|
1170
|
+
const ruleEntry = toRuleStatEntry(ruleStatsSnapshot[rule]);
|
|
1171
|
+
if (ruleEntry.count > 5 && ruleEntry.falsePositives > 0) {
|
|
1172
|
+
const rate = Math.round(ruleEntry.falsePositives / ruleEntry.count * 100);
|
|
1173
|
+
if (rate > 40) {
|
|
1174
|
+
console.log(chalk.yellow(`
|
|
1175
|
+
Noisy: ${rate}% historical false-positive rate`));
|
|
1176
|
+
console.log(chalk.dim(` Silence: memory-core allow "${rule}"`));
|
|
1177
|
+
console.log(chalk.dim(` Review all: memory-core tune`));
|
|
1178
|
+
} else if (rate > 25) {
|
|
1179
|
+
console.log(chalk.dim(`
|
|
1180
|
+
Note: ${rate}% false-positive rate \u2014 run: memory-core tune`));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
845
1183
|
console.log();
|
|
846
|
-
}
|
|
1184
|
+
}
|
|
847
1185
|
console.log(chalk.dim(" Fix the violations above, then commit again."));
|
|
848
1186
|
console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
|
|
849
1187
|
console.log(chalk.dim(" Env bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
|
|
@@ -1031,35 +1369,6 @@ function printBanner(projectName, agentCount, status) {
|
|
|
1031
1369
|
];
|
|
1032
1370
|
lines.forEach((l) => console.log(l));
|
|
1033
1371
|
}
|
|
1034
|
-
async function checkConnections(dbUrl, ollamaUrl, chatModel) {
|
|
1035
|
-
const spinner = ora("Checking connections\u2026").start();
|
|
1036
|
-
let postgresOk = false;
|
|
1037
|
-
let ollamaOk = false;
|
|
1038
|
-
try {
|
|
1039
|
-
const { Pool } = (await import("pg")).default;
|
|
1040
|
-
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
1041
|
-
await testPool.query("SELECT 1");
|
|
1042
|
-
await testPool.end();
|
|
1043
|
-
postgresOk = true;
|
|
1044
|
-
} catch {
|
|
1045
|
-
postgresOk = false;
|
|
1046
|
-
}
|
|
1047
|
-
try {
|
|
1048
|
-
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1049
|
-
ollamaOk = res.ok;
|
|
1050
|
-
} catch {
|
|
1051
|
-
ollamaOk = false;
|
|
1052
|
-
}
|
|
1053
|
-
spinner.stop();
|
|
1054
|
-
console.log(
|
|
1055
|
-
postgresOk ? chalk2.green(" \u2713 PostgreSQL") + chalk2.dim(" \u2014 connected") : chalk2.red(" \u2717 PostgreSQL") + chalk2.dim(" \u2014 cannot connect. Check DATABASE_URL and that PostgreSQL is running.")
|
|
1056
|
-
);
|
|
1057
|
-
console.log(
|
|
1058
|
-
ollamaOk ? chalk2.green(" \u2713 Ollama ") + chalk2.dim(` \u2014 connected (${chatModel})`) : chalk2.red(" \u2717 Ollama ") + chalk2.dim(" \u2014 not reachable. Run: ollama serve")
|
|
1059
|
-
);
|
|
1060
|
-
console.log();
|
|
1061
|
-
return { postgresOk, ollamaOk, chatModel };
|
|
1062
|
-
}
|
|
1063
1372
|
var { version } = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1064
1373
|
var CONFIG_FILE = ".memory-core.json";
|
|
1065
1374
|
var LOCAL_GENERATED_FILES = [".memory-core-stats.json"];
|
|
@@ -1108,6 +1417,7 @@ function writeRuntimeEnv(values, envPath = getEnvPath()) {
|
|
|
1108
1417
|
"OLLAMA_MODEL",
|
|
1109
1418
|
"CHAT_PROVIDER",
|
|
1110
1419
|
"CHAT_MODEL",
|
|
1420
|
+
"CHAT_BASE_URL",
|
|
1111
1421
|
"OLLAMA_CHAT_MODEL",
|
|
1112
1422
|
"CHAT_API_KEY"
|
|
1113
1423
|
];
|
|
@@ -1206,10 +1516,10 @@ function removeProjectFiles(relativePaths) {
|
|
|
1206
1516
|
}
|
|
1207
1517
|
function normalizeProvider(value) {
|
|
1208
1518
|
const provider2 = value.trim().toLowerCase();
|
|
1209
|
-
if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
|
|
1519
|
+
if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax" || provider2 === "openai-compatible") {
|
|
1210
1520
|
return provider2;
|
|
1211
1521
|
}
|
|
1212
|
-
throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax`);
|
|
1522
|
+
throw new Error(`Unsupported provider "${value}". Use: ollama, openai, anthropic, minimax, openai-compatible`);
|
|
1213
1523
|
}
|
|
1214
1524
|
function providerLabel(provider2) {
|
|
1215
1525
|
switch (provider2) {
|
|
@@ -1219,6 +1529,8 @@ function providerLabel(provider2) {
|
|
|
1219
1529
|
return "Anthropic";
|
|
1220
1530
|
case "minimax":
|
|
1221
1531
|
return "MiniMax";
|
|
1532
|
+
case "openai-compatible":
|
|
1533
|
+
return "OpenAI-compatible";
|
|
1222
1534
|
default:
|
|
1223
1535
|
return "Ollama";
|
|
1224
1536
|
}
|
|
@@ -1305,6 +1617,11 @@ function buildMemoryContext(opts) {
|
|
|
1305
1617
|
if (opts.source?.trim()) context.source = opts.source.trim();
|
|
1306
1618
|
return Object.keys(context).length ? context : void 0;
|
|
1307
1619
|
}
|
|
1620
|
+
function memoryReasonOrFallback(reason, content, type = "memory") {
|
|
1621
|
+
const trimmed = reason?.trim();
|
|
1622
|
+
if (trimmed) return trimmed;
|
|
1623
|
+
return `Captured as a ${type} memory because it should be remembered: ${content}`;
|
|
1624
|
+
}
|
|
1308
1625
|
function truncate(value, length) {
|
|
1309
1626
|
if (!value) return "";
|
|
1310
1627
|
return value.length > length ? `${value.slice(0, Math.max(0, length - 1))}\u2026` : value;
|
|
@@ -1596,16 +1913,56 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1596
1913
|
console.log(chalk2.bold.cyan("\n memory-core init\n"));
|
|
1597
1914
|
const detected = detectProject();
|
|
1598
1915
|
const quick = opts.quick ?? false;
|
|
1916
|
+
let skipEnv = false;
|
|
1917
|
+
let skipProject = false;
|
|
1918
|
+
if (existsSync3(join3(process.cwd(), CONFIG_FILE)) && !quick) {
|
|
1919
|
+
const existing = readProjectConfig();
|
|
1920
|
+
const envVals = readRuntimeEnv().values;
|
|
1921
|
+
console.log(chalk2.dim(` Already initialized: ${existing?.projectName ?? "?"} (${existing?.projectType ?? "?"})`));
|
|
1922
|
+
console.log(chalk2.dim(` Provider: ${envVals.CHAT_PROVIDER ?? "ollama"} Model: ${envVals.CHAT_MODEL ?? "llama3.2"}`));
|
|
1923
|
+
console.log(chalk2.dim(` Hook: ${existsSync3(join3(".git", "hooks", "pre-commit")) ? "installed" : "not installed"} Agents: ${existing?.agents?.length ?? 0}
|
|
1924
|
+
`));
|
|
1925
|
+
const reinitChoice = await select({
|
|
1926
|
+
message: "Already initialized \u2014 what do you want to do?",
|
|
1927
|
+
choices: [
|
|
1928
|
+
{ value: "full", name: "Full re-init \u2014 update everything" },
|
|
1929
|
+
{ value: "connection", name: "Connection only \u2014 update DB / provider / model" },
|
|
1930
|
+
{ value: "project", name: "Project only \u2014 update arch, agents, hook" },
|
|
1931
|
+
{ value: "cancel", name: "Cancel" }
|
|
1932
|
+
]
|
|
1933
|
+
});
|
|
1934
|
+
if (reinitChoice === "cancel") {
|
|
1935
|
+
await closePool();
|
|
1936
|
+
process.exit(0);
|
|
1937
|
+
}
|
|
1938
|
+
skipEnv = reinitChoice === "project";
|
|
1939
|
+
skipProject = reinitChoice === "connection";
|
|
1940
|
+
}
|
|
1941
|
+
let pgOk = false;
|
|
1942
|
+
let ollamaOk = false;
|
|
1599
1943
|
const envPath = join3(process.cwd(), ".memory-core.env");
|
|
1600
1944
|
const hasEnv = existsSync3(envPath) || existsSync3(join3(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
|
|
1601
|
-
if (
|
|
1945
|
+
if (skipEnv) {
|
|
1946
|
+
try {
|
|
1947
|
+
const { Pool } = (await import("pg")).default;
|
|
1948
|
+
const p = new Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3e3 });
|
|
1949
|
+
await p.query("SELECT 1");
|
|
1950
|
+
await p.end();
|
|
1951
|
+
pgOk = true;
|
|
1952
|
+
} catch {
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
const r = await fetch(`${process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3e3) });
|
|
1956
|
+
ollamaOk = r.ok;
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
} else if (!hasEnv && quick) {
|
|
1602
1960
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1603
1961
|
const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
|
|
1604
|
-
const ollamaUrl = DEFAULT_OLLAMA_URL;
|
|
1605
1962
|
const chatModel = DEFAULT_CHAT_MODEL;
|
|
1606
1963
|
const envValues = {
|
|
1607
1964
|
DATABASE_URL: dbUrl,
|
|
1608
|
-
OLLAMA_URL:
|
|
1965
|
+
OLLAMA_URL: DEFAULT_OLLAMA_URL,
|
|
1609
1966
|
OLLAMA_MODEL: DEFAULT_EMBEDDING_MODEL,
|
|
1610
1967
|
CHAT_PROVIDER: "ollama",
|
|
1611
1968
|
CHAT_MODEL: chatModel,
|
|
@@ -1631,6 +1988,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1631
1988
|
await testPool.query("SELECT 1");
|
|
1632
1989
|
await testPool.end();
|
|
1633
1990
|
pgSpinner.succeed(chalk2.green("PostgreSQL connected"));
|
|
1991
|
+
pgOk = true;
|
|
1634
1992
|
break;
|
|
1635
1993
|
} catch (err) {
|
|
1636
1994
|
pgSpinner.fail(chalk2.red(`Cannot connect: ${err.message}`));
|
|
@@ -1640,14 +1998,15 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1640
1998
|
let ollamaUrl = "";
|
|
1641
1999
|
while (true) {
|
|
1642
2000
|
ollamaUrl = await input({
|
|
1643
|
-
message: "Ollama URL?",
|
|
1644
|
-
default: ollamaUrl ||
|
|
2001
|
+
message: "Ollama URL (used for search embeddings)?",
|
|
2002
|
+
default: ollamaUrl || DEFAULT_OLLAMA_URL
|
|
1645
2003
|
});
|
|
1646
2004
|
const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
|
|
1647
2005
|
try {
|
|
1648
2006
|
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1649
2007
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1650
2008
|
ollamaSpinner.succeed(chalk2.green("Ollama connected"));
|
|
2009
|
+
ollamaOk = true;
|
|
1651
2010
|
break;
|
|
1652
2011
|
} catch (err) {
|
|
1653
2012
|
ollamaSpinner.fail(chalk2.red(`Cannot reach Ollama: ${err.message}`));
|
|
@@ -1660,11 +2019,16 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1660
2019
|
{ name: "Local \u2014 Ollama (no API key, free)", value: "ollama" },
|
|
1661
2020
|
{ name: "OpenAI \u2014 gpt-4o, gpt-4o-mini", value: "openai" },
|
|
1662
2021
|
{ name: "Anthropic \u2014 claude-sonnet, claude-haiku", value: "anthropic" },
|
|
1663
|
-
{ name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" }
|
|
2022
|
+
{ name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" },
|
|
2023
|
+
{ name: "OpenAI-compatible \u2014 Groq, DeepSeek, xAI, Mistral, Together\u2026", value: "openai-compatible" }
|
|
1664
2024
|
]
|
|
1665
2025
|
});
|
|
2026
|
+
if (chatProvider !== "ollama") {
|
|
2027
|
+
console.log(chalk2.dim(" Note: Ollama is still used for search embeddings. Code checking uses the cloud provider above."));
|
|
2028
|
+
}
|
|
1666
2029
|
let chatModel = "";
|
|
1667
2030
|
let chatApiKey = "";
|
|
2031
|
+
let chatBaseUrl = "";
|
|
1668
2032
|
if (chatProvider === "ollama") {
|
|
1669
2033
|
while (true) {
|
|
1670
2034
|
const chatModelChoice = await select({
|
|
@@ -1717,6 +2081,13 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1717
2081
|
{ name: "MiniMax-Text-01 (flagship)", value: "MiniMax-Text-01" },
|
|
1718
2082
|
{ name: "abab6.5s-chat (fast, efficient)", value: "abab6.5s-chat" },
|
|
1719
2083
|
{ name: "Other (enter manually)", value: "__custom__" }
|
|
2084
|
+
],
|
|
2085
|
+
"openai-compatible": [
|
|
2086
|
+
{ name: "llama-3.1-70b-versatile (Groq)", value: "llama-3.1-70b-versatile" },
|
|
2087
|
+
{ name: "deepseek-coder (DeepSeek)", value: "deepseek-coder" },
|
|
2088
|
+
{ name: "grok-beta (xAI)", value: "grok-beta" },
|
|
2089
|
+
{ name: "mistral-large-latest (Mistral)", value: "mistral-large-latest" },
|
|
2090
|
+
{ name: "Other (enter manually)", value: "__custom__" }
|
|
1720
2091
|
]
|
|
1721
2092
|
};
|
|
1722
2093
|
const modelChoice = await select({
|
|
@@ -1724,8 +2095,14 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1724
2095
|
choices: modelChoices[chatProvider]
|
|
1725
2096
|
});
|
|
1726
2097
|
chatModel = modelChoice === "__custom__" ? await input({ message: "Model name?" }) : modelChoice;
|
|
2098
|
+
if (chatProvider === "openai-compatible") {
|
|
2099
|
+
chatBaseUrl = await input({
|
|
2100
|
+
message: "API base URL?",
|
|
2101
|
+
default: "https://api.groq.com/openai/v1"
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
1727
2104
|
chatApiKey = await input({
|
|
1728
|
-
message: `${
|
|
2105
|
+
message: `${providerLabel(chatProvider)} API key?`
|
|
1729
2106
|
});
|
|
1730
2107
|
console.log(chalk2.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
|
|
1731
2108
|
}
|
|
@@ -1737,19 +2114,34 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1737
2114
|
CHAT_MODEL: chatModel
|
|
1738
2115
|
};
|
|
1739
2116
|
if (chatProvider === "ollama") envValues.OLLAMA_CHAT_MODEL = chatModel;
|
|
2117
|
+
if (chatBaseUrl) envValues.CHAT_BASE_URL = chatBaseUrl;
|
|
1740
2118
|
if (chatApiKey) envValues.CHAT_API_KEY = chatApiKey;
|
|
1741
2119
|
writeRuntimeEnv(envValues, envPath);
|
|
1742
2120
|
applyRuntimeEnv(envValues);
|
|
1743
2121
|
ensureEnvFileIgnored(envPath);
|
|
1744
2122
|
console.log(chalk2.green("\n \u2713 .memory-core.env created"));
|
|
1745
2123
|
console.log(chalk2.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
2124
|
+
} else {
|
|
2125
|
+
try {
|
|
2126
|
+
const { Pool } = (await import("pg")).default;
|
|
2127
|
+
const p = new Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3e3 });
|
|
2128
|
+
await p.query("SELECT 1");
|
|
2129
|
+
await p.end();
|
|
2130
|
+
pgOk = true;
|
|
2131
|
+
} catch {
|
|
2132
|
+
}
|
|
2133
|
+
try {
|
|
2134
|
+
const r = await fetch(`${process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3e3) });
|
|
2135
|
+
ollamaOk = r.ok;
|
|
2136
|
+
} catch {
|
|
2137
|
+
}
|
|
1746
2138
|
}
|
|
1747
|
-
const projectName = quick ? process.cwd().split("/").pop() ?? "my-project" : await input({
|
|
2139
|
+
const projectName = quick || skipProject ? readProjectConfig()?.projectName ?? process.cwd().split("/").pop() ?? "my-project" : await input({
|
|
1748
2140
|
message: "Project name?",
|
|
1749
2141
|
default: process.cwd().split("/").pop() ?? "my-project"
|
|
1750
2142
|
});
|
|
1751
2143
|
const inferredProjectType = ["Nuxt.js"].includes(detected.framework) ? "fullstack" : ["React", "Vue.js", "Svelte"].includes(detected.framework) ? "frontend" : "backend";
|
|
1752
|
-
const projectType = quick ? inferredProjectType : await select({
|
|
2144
|
+
const projectType = quick || skipProject ? readProjectConfig()?.projectType ?? inferredProjectType : await select({
|
|
1753
2145
|
message: "Project type?",
|
|
1754
2146
|
choices: [
|
|
1755
2147
|
{ value: "backend", name: "Backend \u2014 API, server, microservice" },
|
|
@@ -1759,8 +2151,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1759
2151
|
});
|
|
1760
2152
|
let backendArchitecture;
|
|
1761
2153
|
if (projectType === "backend" || projectType === "fullstack") {
|
|
1762
|
-
if (quick) {
|
|
1763
|
-
backendArchitecture = detected.framework === "NestJS" ? "nestjs" : detected.framework === "Laravel" ? "laravel-service-repository" : detected.framework === "Go" ? "go-api" : "clean-architecture";
|
|
2154
|
+
if (quick || skipProject) {
|
|
2155
|
+
backendArchitecture = readProjectConfig()?.backendArchitecture ?? (detected.framework === "NestJS" ? "nestjs" : detected.framework === "Laravel" ? "laravel-service-repository" : detected.framework === "Go" ? "go-api" : "clean-architecture");
|
|
1764
2156
|
} else {
|
|
1765
2157
|
const backendProfiles = listProfiles("backend");
|
|
1766
2158
|
backendArchitecture = await select({
|
|
@@ -1774,14 +2166,14 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1774
2166
|
}
|
|
1775
2167
|
let frontendFramework;
|
|
1776
2168
|
if (projectType === "frontend" || projectType === "fullstack") {
|
|
1777
|
-
if (quick) {
|
|
2169
|
+
if (quick || skipProject) {
|
|
1778
2170
|
const frameworkMap = {
|
|
1779
2171
|
"Nuxt.js": "nuxt",
|
|
1780
2172
|
React: "react",
|
|
1781
2173
|
"Vue.js": "vue",
|
|
1782
2174
|
Svelte: "svelte"
|
|
1783
2175
|
};
|
|
1784
|
-
frontendFramework = frameworkMap[detected.framework] ?? "react";
|
|
2176
|
+
frontendFramework = readProjectConfig()?.frontendFramework ?? (frameworkMap[detected.framework] ?? "react");
|
|
1785
2177
|
} else {
|
|
1786
2178
|
const frontendProfiles = listProfiles("frontend");
|
|
1787
2179
|
frontendFramework = await select({
|
|
@@ -1793,43 +2185,20 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1793
2185
|
});
|
|
1794
2186
|
}
|
|
1795
2187
|
}
|
|
1796
|
-
const language = quick ? detected.language : await input({
|
|
2188
|
+
const language = quick || skipProject ? readProjectConfig()?.language ?? detected.language : await input({
|
|
1797
2189
|
message: "Language?",
|
|
1798
2190
|
default: detected.language
|
|
1799
2191
|
});
|
|
1800
|
-
const pullMemories = quick ? true : await confirm({
|
|
1801
|
-
message: "Pull relevant memories from previous projects?",
|
|
1802
|
-
default: true
|
|
1803
|
-
});
|
|
1804
|
-
let installCaveman = quick ? false : await confirm({
|
|
1805
|
-
message: "Install caveman token saver? Downloads and runs the upstream installer.",
|
|
1806
|
-
default: false
|
|
1807
|
-
});
|
|
1808
|
-
let cavemanIntensity = "full";
|
|
1809
|
-
if (installCaveman) {
|
|
1810
|
-
const allowRemoteInstaller = await confirm({
|
|
1811
|
-
message: `Security check: download and execute installer from ${CAVEMAN_INSTALL_URL}?`,
|
|
1812
|
-
default: false
|
|
1813
|
-
});
|
|
1814
|
-
if (!allowRemoteInstaller) {
|
|
1815
|
-
installCaveman = false;
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
if (installCaveman) {
|
|
1819
|
-
cavemanIntensity = await select({
|
|
1820
|
-
message: "Caveman intensity?",
|
|
1821
|
-
choices: [
|
|
1822
|
-
{ value: "full", name: "Full \u2014 caveman mode (default)" },
|
|
1823
|
-
{ value: "lite", name: "Lite \u2014 professional terseness" },
|
|
1824
|
-
{ value: "ultra", name: "Ultra \u2014 telegraphic, minimum words" }
|
|
1825
|
-
]
|
|
1826
|
-
});
|
|
1827
|
-
}
|
|
1828
2192
|
const selectedAgents = quick ? AGENT_NAMES.filter((a) => a !== "Shared") : await (async () => {
|
|
1829
2193
|
const { checkbox } = await import("@inquirer/prompts");
|
|
2194
|
+
const saved = new Set(readProjectConfig()?.agents ?? []);
|
|
1830
2195
|
return checkbox({
|
|
1831
2196
|
message: "Which AI agents do you want to generate files for?",
|
|
1832
|
-
choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
|
|
2197
|
+
choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
|
|
2198
|
+
name,
|
|
2199
|
+
value: name,
|
|
2200
|
+
checked: saved.size === 0 || saved.has(name)
|
|
2201
|
+
})),
|
|
1833
2202
|
instructions: " (Space to toggle, A to select all, Enter to confirm)"
|
|
1834
2203
|
});
|
|
1835
2204
|
})();
|
|
@@ -1848,6 +2217,42 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1848
2217
|
]
|
|
1849
2218
|
});
|
|
1850
2219
|
}
|
|
2220
|
+
let installCaveman = false;
|
|
2221
|
+
let cavemanIntensity = "full";
|
|
2222
|
+
if (!quick) {
|
|
2223
|
+
installCaveman = await confirm({
|
|
2224
|
+
message: "Enable caveman token saver? (compresses AI responses ~70%)",
|
|
2225
|
+
default: false
|
|
2226
|
+
});
|
|
2227
|
+
if (installCaveman) {
|
|
2228
|
+
cavemanIntensity = await select({
|
|
2229
|
+
message: "Intensity?",
|
|
2230
|
+
choices: [
|
|
2231
|
+
{ value: "full", name: "Full \u2014 caveman mode (default)" },
|
|
2232
|
+
{ value: "lite", name: "Lite \u2014 professional terseness" },
|
|
2233
|
+
{ value: "ultra", name: "Ultra \u2014 telegraphic, minimum words" }
|
|
2234
|
+
]
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
if (!quick) {
|
|
2239
|
+
const envVals = readRuntimeEnv().values;
|
|
2240
|
+
console.log(chalk2.bold("\n Ready to initialize\n"));
|
|
2241
|
+
console.log(` Project ${chalk2.white(projectName)} (${projectType})`);
|
|
2242
|
+
if (backendArchitecture) console.log(` Backend ${chalk2.white(backendArchitecture)}`);
|
|
2243
|
+
if (frontendFramework) console.log(` Frontend ${chalk2.white(frontendFramework)}`);
|
|
2244
|
+
console.log(` Language ${chalk2.white(language)}`);
|
|
2245
|
+
console.log(` Provider ${chalk2.white(envVals.CHAT_PROVIDER ?? "ollama")} / ${chalk2.white(envVals.CHAT_MODEL ?? DEFAULT_CHAT_MODEL)}`);
|
|
2246
|
+
console.log(` Agents ${chalk2.white(String(selectedAgents.length))} selected`);
|
|
2247
|
+
console.log(` Hook ${chalk2.white(enableHook ? hookAdvisory ? "advisory" : "strict" : "skip")}`);
|
|
2248
|
+
console.log();
|
|
2249
|
+
const proceed = await confirm({ message: "Generate files?", default: true });
|
|
2250
|
+
if (!proceed) {
|
|
2251
|
+
console.log(chalk2.yellow(" Cancelled.\n"));
|
|
2252
|
+
await closePool();
|
|
2253
|
+
process.exit(0);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
1851
2256
|
const config = {
|
|
1852
2257
|
projectName,
|
|
1853
2258
|
projectType,
|
|
@@ -1856,34 +2261,33 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1856
2261
|
language,
|
|
1857
2262
|
caveman: { enabled: installCaveman, intensity: cavemanIntensity },
|
|
1858
2263
|
agents: selectedAgents,
|
|
1859
|
-
allowPatterns: [],
|
|
2264
|
+
allowPatterns: readProjectConfig()?.allowPatterns ?? [],
|
|
2265
|
+
commitRules: readProjectConfig()?.commitRules,
|
|
1860
2266
|
autoSync: true
|
|
1861
2267
|
};
|
|
1862
2268
|
let memories = [];
|
|
1863
|
-
|
|
1864
|
-
const
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
memories
|
|
1874
|
-
spinner2.succeed(`Found ${memories.length} relevant memories`);
|
|
2269
|
+
try {
|
|
2270
|
+
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
|
|
2271
|
+
const selection = await retrieveMemorySelection({
|
|
2272
|
+
query: archQuery,
|
|
2273
|
+
cwd: process.cwd(),
|
|
2274
|
+
config,
|
|
2275
|
+
limit: 20
|
|
2276
|
+
});
|
|
2277
|
+
memories = selection.included;
|
|
2278
|
+
if (memories.length > 0) {
|
|
2279
|
+
console.log(chalk2.dim(` Found ${memories.length} relevant memories`));
|
|
1875
2280
|
printMemorySelection(selection);
|
|
1876
|
-
} catch (err) {
|
|
1877
|
-
spinner2.warn(`Could not retrieve memories: ${err.message}`);
|
|
1878
2281
|
}
|
|
2282
|
+
} catch {
|
|
1879
2283
|
}
|
|
1880
2284
|
if (installCaveman) {
|
|
1881
|
-
const
|
|
2285
|
+
const cavemanSpinner = ora("Installing caveman token saver\u2026").start();
|
|
1882
2286
|
try {
|
|
1883
2287
|
await installCavemanTokenSaver();
|
|
1884
|
-
|
|
2288
|
+
cavemanSpinner.succeed("Caveman installed");
|
|
1885
2289
|
} catch (err) {
|
|
1886
|
-
|
|
2290
|
+
cavemanSpinner.warn(`Caveman install failed: ${err.message}`);
|
|
1887
2291
|
}
|
|
1888
2292
|
}
|
|
1889
2293
|
const spinner = ora("Generating AI agent context files\u2026").start();
|
|
@@ -1904,12 +2308,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1904
2308
|
if (enableHook) {
|
|
1905
2309
|
installHook(hookAdvisory);
|
|
1906
2310
|
}
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
1910
|
-
process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"
|
|
1911
|
-
);
|
|
1912
|
-
printBanner(config.projectName, written.written.length, status);
|
|
2311
|
+
const chatModelForBanner = process.env.CHAT_MODEL ?? DEFAULT_CHAT_MODEL;
|
|
2312
|
+
printBanner(config.projectName, written.written.length, { postgresOk: pgOk, ollamaOk, chatModel: chatModelForBanner });
|
|
1913
2313
|
await closePool();
|
|
1914
2314
|
});
|
|
1915
2315
|
program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
|
|
@@ -1960,27 +2360,31 @@ program.command("auto-sync [mode]").description("Show or change automatic agent
|
|
|
1960
2360
|
});
|
|
1961
2361
|
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
2362
|
const config = readProjectConfig();
|
|
2363
|
+
const scope = opts.scope?.trim() || "project";
|
|
1963
2364
|
let reason = opts.reason;
|
|
1964
2365
|
if (!reason) {
|
|
1965
2366
|
reason = await input({
|
|
1966
|
-
message: chalk2.dim("Why
|
|
2367
|
+
message: chalk2.dim("Why should this memory exist?"),
|
|
1967
2368
|
default: ""
|
|
1968
2369
|
});
|
|
1969
2370
|
}
|
|
2371
|
+
const storedReason = memoryReasonOrFallback(reason, text, opts.type);
|
|
1970
2372
|
const spinner = ora("Saving memory\u2026").start();
|
|
1971
2373
|
try {
|
|
1972
2374
|
await phase1.services.memoryEngine.remember({
|
|
1973
2375
|
type: opts.type,
|
|
1974
|
-
scope
|
|
2376
|
+
scope,
|
|
1975
2377
|
architecture: config?.backendArchitecture ?? config?.frontendFramework,
|
|
1976
|
-
projectName: config?.projectName,
|
|
2378
|
+
projectName: scope === "project" ? config?.projectName : void 0,
|
|
1977
2379
|
content: text,
|
|
1978
|
-
reason:
|
|
2380
|
+
reason: storedReason,
|
|
1979
2381
|
context: buildMemoryContext(opts),
|
|
1980
2382
|
tags: parseTags(opts.tags)
|
|
1981
2383
|
});
|
|
1982
|
-
const
|
|
1983
|
-
|
|
2384
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2385
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2386
|
+
const reasonLine = chalk2.gray(`
|
|
2387
|
+
Why: ${storedReason}`);
|
|
1984
2388
|
spinner.succeed(chalk2.green(`Memory saved: "${text}"`) + reasonLine);
|
|
1985
2389
|
await autoSyncGeneratedFiles(config, "remember", opts.sync);
|
|
1986
2390
|
} catch (err) {
|
|
@@ -2067,6 +2471,8 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2067
2471
|
}
|
|
2068
2472
|
spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
|
|
2069
2473
|
if (inserted > 0) {
|
|
2474
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2475
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2070
2476
|
await autoSyncGeneratedFiles(config, "import", opts.sync);
|
|
2071
2477
|
}
|
|
2072
2478
|
} catch (err) {
|
|
@@ -2162,7 +2568,7 @@ program.command("edit <id>").description("Edit a memory interactively").option("
|
|
|
2162
2568
|
scope,
|
|
2163
2569
|
title: title || void 0,
|
|
2164
2570
|
content,
|
|
2165
|
-
reason: reason
|
|
2571
|
+
reason: memoryReasonOrFallback(reason, content, type),
|
|
2166
2572
|
context: buildMemoryContext({ appliesTo, avoidWhen, example: examples, source }),
|
|
2167
2573
|
tags: parseTags(tags)
|
|
2168
2574
|
});
|
|
@@ -2205,6 +2611,8 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
2205
2611
|
content: pattern,
|
|
2206
2612
|
tags: ["ignore"]
|
|
2207
2613
|
});
|
|
2614
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
2615
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2208
2616
|
console.log(chalk2.green(`Ignored pattern saved: "${pattern}"`));
|
|
2209
2617
|
await autoSyncGeneratedFiles(config, "ignore", opts.sync);
|
|
2210
2618
|
} catch (err) {
|
|
@@ -2244,6 +2652,61 @@ program.command("allow [pattern]").description("Manage project allow patterns in
|
|
|
2244
2652
|
}));
|
|
2245
2653
|
console.log(chalk2.green(`Allow pattern saved: "${pattern}"`));
|
|
2246
2654
|
});
|
|
2655
|
+
program.command("commit-rules [pattern]").description("Manage commit message rules in .memory-core.json").option("--message <msg>", "Error message shown when rule is violated (required when adding)").option("--negate", "Pattern must NOT match (default: must match)").option("--advisory", "Warn only \u2014 do not block commit").option("--list", "List current commit rules").option("--remove <pattern>", "Remove a commit rule by pattern").action((pattern, opts) => {
|
|
2656
|
+
if (opts.list) {
|
|
2657
|
+
const rules = readProjectConfig()?.commitRules ?? [];
|
|
2658
|
+
if (rules.length === 0) {
|
|
2659
|
+
console.log(chalk2.yellow("\n No commit rules configured.\n"));
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
console.log(chalk2.bold("\n Commit message rules\n"));
|
|
2663
|
+
rules.forEach((rule, i) => {
|
|
2664
|
+
const flags = [
|
|
2665
|
+
rule.negate ? "must NOT match" : "must match",
|
|
2666
|
+
rule.advisory ? "advisory" : "blocking"
|
|
2667
|
+
].join(", ");
|
|
2668
|
+
console.log(` ${i + 1}. ${rule.pattern}`);
|
|
2669
|
+
console.log(chalk2.dim(` Message: ${rule.message}`));
|
|
2670
|
+
console.log(chalk2.dim(` Flags: ${flags}`));
|
|
2671
|
+
console.log();
|
|
2672
|
+
});
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (opts.remove) {
|
|
2676
|
+
updateProjectConfig((config) => ({
|
|
2677
|
+
...config,
|
|
2678
|
+
commitRules: (config.commitRules ?? []).filter((r) => r.pattern !== opts.remove)
|
|
2679
|
+
}));
|
|
2680
|
+
console.log(chalk2.green(`Commit rule removed: "${opts.remove}"`));
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
if (!pattern) {
|
|
2684
|
+
console.error(chalk2.red("Provide a pattern, --list, or --remove <pattern>"));
|
|
2685
|
+
process.exit(1);
|
|
2686
|
+
}
|
|
2687
|
+
if (!opts.message) {
|
|
2688
|
+
console.error(chalk2.red("--message is required when adding a commit rule"));
|
|
2689
|
+
process.exit(1);
|
|
2690
|
+
}
|
|
2691
|
+
try {
|
|
2692
|
+
new RegExp(pattern);
|
|
2693
|
+
} catch {
|
|
2694
|
+
console.error(chalk2.red(`Invalid regex pattern: "${pattern}"`));
|
|
2695
|
+
process.exit(1);
|
|
2696
|
+
}
|
|
2697
|
+
const newRule = {
|
|
2698
|
+
pattern,
|
|
2699
|
+
message: opts.message,
|
|
2700
|
+
...opts.negate && { negate: true },
|
|
2701
|
+
...opts.advisory && { advisory: true }
|
|
2702
|
+
};
|
|
2703
|
+
updateProjectConfig((config) => ({
|
|
2704
|
+
...config,
|
|
2705
|
+
commitRules: [...(config.commitRules ?? []).filter((r) => r.pattern !== pattern), newRule]
|
|
2706
|
+
}));
|
|
2707
|
+
console.log(chalk2.green(`Commit rule saved: "${pattern}"`));
|
|
2708
|
+
console.log(chalk2.dim(" Run: memory-core commit-rules --list to see all rules"));
|
|
2709
|
+
});
|
|
2247
2710
|
program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
|
|
2248
2711
|
const workflowPath = join3(process.cwd(), ".github", "workflows", "memory-core.yml");
|
|
2249
2712
|
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
@@ -2308,7 +2771,7 @@ program.command("uninstall").description("Remove memory-core from the current pr
|
|
|
2308
2771
|
console.log(chalk2.gray(" \u2713 cleaned .gitignore memory-core block"));
|
|
2309
2772
|
}
|
|
2310
2773
|
});
|
|
2311
|
-
program.command("stats").description("Show violation counters recorded by check and watch").option("--reset", "Reset violation counters and recent history").action((opts) => {
|
|
2774
|
+
program.command("stats").description("Show violation counters recorded by check and watch").option("--reset", "Reset violation counters and recent history").option("--tune", "Show only noisy rules (>40% false-positive rate) with disable commands").action((opts) => {
|
|
2312
2775
|
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
2313
2776
|
if (opts.reset) {
|
|
2314
2777
|
const emptyStats = {
|
|
@@ -2330,14 +2793,46 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2330
2793
|
return;
|
|
2331
2794
|
}
|
|
2332
2795
|
const stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
|
|
2796
|
+
const toEntry = (raw) => {
|
|
2797
|
+
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
2798
|
+
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2799
|
+
return raw;
|
|
2800
|
+
};
|
|
2333
2801
|
const printTop = (label, values = {}) => {
|
|
2334
2802
|
console.log(chalk2.bold(`
|
|
2335
2803
|
${label}
|
|
2336
2804
|
`));
|
|
2337
|
-
Object.entries(values).sort((a, b) => b
|
|
2338
|
-
|
|
2805
|
+
Object.entries(values).map(([name, raw]) => ({ name, entry: toEntry(raw) })).sort((a, b) => b.entry.count - a.entry.count).slice(0, 10).forEach(({ name, entry }, index) => {
|
|
2806
|
+
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2807
|
+
const fpHint = rate > 0 ? chalk2.dim(` \u2014 ${rate}% false-positive rate`) + (rate > 25 ? " \u26A0\uFE0F" : "") : "";
|
|
2808
|
+
console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${entry.count} hits${fpHint}`);
|
|
2339
2809
|
});
|
|
2340
2810
|
};
|
|
2811
|
+
if (opts.tune) {
|
|
2812
|
+
const tuneThreshold = 40;
|
|
2813
|
+
const tuneMinCount = 5;
|
|
2814
|
+
const noisy = Object.entries(stats.rules ?? {}).map(([name, raw]) => ({ name, entry: toEntry(raw) })).map(({ name, entry }) => ({
|
|
2815
|
+
name,
|
|
2816
|
+
count: entry.count,
|
|
2817
|
+
rate: entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0
|
|
2818
|
+
})).filter((r) => r.rate > tuneThreshold && r.count >= tuneMinCount).sort((a, b) => b.rate - a.rate);
|
|
2819
|
+
if (noisy.length === 0) {
|
|
2820
|
+
console.log(chalk2.green(`
|
|
2821
|
+
\u2713 No noisy rules found (threshold: ${tuneThreshold}%, min hits: ${tuneMinCount})
|
|
2822
|
+
`));
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
console.log(chalk2.bold(`
|
|
2826
|
+
Noisy rules (>${tuneThreshold}% false-positive rate, \u2265${tuneMinCount} hits)
|
|
2827
|
+
`));
|
|
2828
|
+
noisy.forEach(({ name, count, rate }, i) => {
|
|
2829
|
+
console.log(` ${i + 1}. ${truncate(name, 50).padEnd(52)} ${count} hits \u2014 ${rate}% \u26A0\uFE0F`);
|
|
2830
|
+
console.log(chalk2.dim(` To disable: memory-core allow "${name}"`));
|
|
2831
|
+
console.log(chalk2.dim(` Interactive: memory-core tune`));
|
|
2832
|
+
console.log();
|
|
2833
|
+
});
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2341
2836
|
const liveRules = stats.live?.rules ?? {};
|
|
2342
2837
|
const liveFiles = stats.live?.files ?? {};
|
|
2343
2838
|
const hasLiveState = !!stats.live;
|
|
@@ -2361,6 +2856,87 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2361
2856
|
}
|
|
2362
2857
|
}
|
|
2363
2858
|
});
|
|
2859
|
+
program.command("tune").description("Review and disable noisy rules with high false-positive rates").option("--threshold <percent>", "False-positive rate % above which a rule is noisy (default: 40)", "40").option("--min-count <n>", "Minimum hit count required to consider a rule (default: 5)", "5").option("--yes", "Disable all noisy rules without prompting").action(async (opts) => {
|
|
2860
|
+
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
2861
|
+
if (!existsSync3(statsPath)) {
|
|
2862
|
+
console.log(chalk2.yellow("\n No violation stats yet. Run some commits first.\n"));
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const threshold = Math.max(0, Math.min(100, parseInt(opts.threshold, 10) || 40));
|
|
2866
|
+
const minCount = Math.max(1, parseInt(opts.minCount, 10) || 5);
|
|
2867
|
+
const toEntry = (raw) => {
|
|
2868
|
+
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
2869
|
+
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
2870
|
+
return raw;
|
|
2871
|
+
};
|
|
2872
|
+
const stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
|
|
2873
|
+
const noisy = Object.entries(stats.rules ?? {}).map(([rule, raw]) => {
|
|
2874
|
+
const entry = toEntry(raw);
|
|
2875
|
+
const rate = entry.count > 0 ? Math.round(entry.falsePositives / entry.count * 100) : 0;
|
|
2876
|
+
return { rule, count: entry.count, rate };
|
|
2877
|
+
}).filter((r) => r.rate > threshold && r.count >= minCount).sort((a, b) => b.rate - a.rate);
|
|
2878
|
+
if (noisy.length === 0) {
|
|
2879
|
+
console.log(chalk2.green(`
|
|
2880
|
+
\u2713 All rules within acceptable noise (threshold: ${threshold}%, min hits: ${minCount})
|
|
2881
|
+
`));
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
console.log(chalk2.bold(`
|
|
2885
|
+
Found ${noisy.length} noisy rule${noisy.length > 1 ? "s" : ""} (>${threshold}% false-positive rate, \u2265${minCount} hits)
|
|
2886
|
+
`));
|
|
2887
|
+
const existingAllows = new Set(
|
|
2888
|
+
(readProjectConfig()?.allowPatterns ?? []).map((p) => p.toLowerCase())
|
|
2889
|
+
);
|
|
2890
|
+
const toAdd = /* @__PURE__ */ new Set();
|
|
2891
|
+
if (opts.yes) {
|
|
2892
|
+
for (const { rule, count, rate } of noisy) {
|
|
2893
|
+
const key = rule.toLowerCase();
|
|
2894
|
+
if (existingAllows.has(key)) {
|
|
2895
|
+
console.log(chalk2.dim(` \u2022 "${truncate(rule, 56)}" \u2014 already disabled`));
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
toAdd.add(key);
|
|
2899
|
+
console.log(chalk2.green(` \u2713 "${truncate(rule, 56)}"`) + chalk2.dim(` \u2014 ${count} hits, ${rate}% FP rate`));
|
|
2900
|
+
}
|
|
2901
|
+
} else {
|
|
2902
|
+
const { select: select2 } = await import("@inquirer/prompts");
|
|
2903
|
+
for (let i = 0; i < noisy.length; i++) {
|
|
2904
|
+
const { rule, count, rate } = noisy[i];
|
|
2905
|
+
const key = rule.toLowerCase();
|
|
2906
|
+
console.log(chalk2.bold(`
|
|
2907
|
+
[${i + 1}/${noisy.length}] "${truncate(rule, 60)}"`));
|
|
2908
|
+
console.log(chalk2.dim(` ${count} hits \u2014 ${rate}% false-positive rate \u26A0\uFE0F`));
|
|
2909
|
+
if (existingAllows.has(key)) {
|
|
2910
|
+
console.log(chalk2.dim(" Already in allow patterns \u2014 skipping"));
|
|
2911
|
+
continue;
|
|
2912
|
+
}
|
|
2913
|
+
const choice = await select2({
|
|
2914
|
+
message: "What would you like to do?",
|
|
2915
|
+
choices: [
|
|
2916
|
+
{ name: "Disable this rule (add to allow patterns)", value: "disable" },
|
|
2917
|
+
{ name: "Keep it (skip for now)", value: "keep" },
|
|
2918
|
+
{ name: "Quit tuning", value: "quit" }
|
|
2919
|
+
]
|
|
2920
|
+
});
|
|
2921
|
+
if (choice === "quit") break;
|
|
2922
|
+
if (choice === "disable") {
|
|
2923
|
+
toAdd.add(key);
|
|
2924
|
+
console.log(chalk2.green(" \u2713 Marked for disable"));
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
if (toAdd.size > 0) {
|
|
2929
|
+
updateProjectConfig((config) => ({
|
|
2930
|
+
...config,
|
|
2931
|
+
allowPatterns: [.../* @__PURE__ */ new Set([...config.allowPatterns ?? [], ...toAdd])]
|
|
2932
|
+
}));
|
|
2933
|
+
console.log(chalk2.green(`
|
|
2934
|
+
\u2713 Saved ${toAdd.size} allow pattern${toAdd.size > 1 ? "s" : ""} to .memory-core.json`));
|
|
2935
|
+
console.log(chalk2.dim(" These rules will no longer block commits.\n"));
|
|
2936
|
+
} else {
|
|
2937
|
+
console.log(chalk2.dim("\n No changes made.\n"));
|
|
2938
|
+
}
|
|
2939
|
+
});
|
|
2364
2940
|
program.command("dashboard").description("Start the live Svelte dashboard with WebSocket watch events").option("-p, --port <port>", "Dashboard port", "5178").option("--path <dir>", "Directory to watch (default: current directory)").option("--no-watch", "Serve the dashboard without starting file watch").action(async (opts) => {
|
|
2365
2941
|
const resolveDashboardPath = () => {
|
|
2366
2942
|
if (typeof opts.path === "string" && opts.path.trim().length > 0) return opts.path;
|
|
@@ -2378,7 +2954,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
|
|
|
2378
2954
|
}
|
|
2379
2955
|
return void 0;
|
|
2380
2956
|
};
|
|
2381
|
-
const { startDashboard } = await import("./dashboard-server-
|
|
2957
|
+
const { startDashboard } = await import("./dashboard-server-ZBGR4CO7.js");
|
|
2382
2958
|
await startDashboard({
|
|
2383
2959
|
port: parseInt(opts.port, 10),
|
|
2384
2960
|
path: resolveDashboardPath(),
|
|
@@ -2424,6 +3000,10 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
2424
3000
|
skipped++;
|
|
2425
3001
|
}
|
|
2426
3002
|
}
|
|
3003
|
+
if (saved > 0) {
|
|
3004
|
+
const dbVersionPath = join3(process.cwd(), ".memory-core-db-version");
|
|
3005
|
+
writeFileSync3(dbVersionPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
3006
|
+
}
|
|
2427
3007
|
console.log(chalk2.bold.green(`
|
|
2428
3008
|
Done. ${saved} memories seeded, ${skipped} skipped.
|
|
2429
3009
|
`));
|
|
@@ -2541,7 +3121,7 @@ read:
|
|
|
2541
3121
|
await closePool();
|
|
2542
3122
|
});
|
|
2543
3123
|
var provider = program.command("provider").description("Manage the code-checking provider configuration");
|
|
2544
|
-
provider.command("set <name>").description("Set the code-checking provider (ollama, openai, anthropic, minimax)").option("--model <model>", "Chat model to set alongside the provider").option("--api-key <key>", "API key for cloud providers").action(async (name, opts) => {
|
|
3124
|
+
provider.command("set <name>").description("Set the code-checking provider (ollama, openai, anthropic, minimax, openai-compatible)").option("--model <model>", "Chat model to set alongside the provider").option("--api-key <key>", "API key for cloud providers").option("--base-url <url>", "API base URL for openai-compatible providers").action(async (name, opts) => {
|
|
2545
3125
|
try {
|
|
2546
3126
|
const providerName = normalizeProvider(name);
|
|
2547
3127
|
const runtimeEnv = readRuntimeEnv();
|
|
@@ -2559,6 +3139,7 @@ provider.command("set <name>").description("Set the code-checking provider (olla
|
|
|
2559
3139
|
values.CHAT_MODEL = model2;
|
|
2560
3140
|
values.OLLAMA_CHAT_MODEL = model2;
|
|
2561
3141
|
delete values.CHAT_API_KEY;
|
|
3142
|
+
delete values.CHAT_BASE_URL;
|
|
2562
3143
|
} else {
|
|
2563
3144
|
delete values.OLLAMA_CHAT_MODEL;
|
|
2564
3145
|
if (opts.apiKey) {
|
|
@@ -2568,6 +3149,16 @@ provider.command("set <name>").description("Set the code-checking provider (olla
|
|
|
2568
3149
|
message: `${providerLabel(providerName)} API key?`
|
|
2569
3150
|
});
|
|
2570
3151
|
}
|
|
3152
|
+
if (providerName === "openai-compatible") {
|
|
3153
|
+
if (opts.baseUrl) {
|
|
3154
|
+
values.CHAT_BASE_URL = opts.baseUrl;
|
|
3155
|
+
} else if (!values.CHAT_BASE_URL) {
|
|
3156
|
+
values.CHAT_BASE_URL = await input({
|
|
3157
|
+
message: "API base URL?",
|
|
3158
|
+
default: "https://api.groq.com/openai/v1"
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
2571
3162
|
}
|
|
2572
3163
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
2573
3164
|
applyRuntimeEnv(values);
|
|
@@ -2775,14 +3366,20 @@ graph.command("diff <leftSnapshotId> [rightSnapshotId]").description("Diff two s
|
|
|
2775
3366
|
}
|
|
2776
3367
|
});
|
|
2777
3368
|
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) => {
|
|
3369
|
+
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
3370
|
const advisory = opts.strict ? false : true;
|
|
2780
|
-
installHook(advisory);
|
|
3371
|
+
installHook(advisory, opts.fast ?? false);
|
|
2781
3372
|
});
|
|
2782
3373
|
hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
|
|
2783
3374
|
uninstallHook();
|
|
2784
3375
|
});
|
|
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) => {
|
|
3376
|
+
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("--commit-msg [file]", "Check commit message (defaults to .git/COMMIT_EDITMSG)").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) => {
|
|
3377
|
+
if (opts.commitMsg !== void 0) {
|
|
3378
|
+
const msgFile = typeof opts.commitMsg === "string" ? opts.commitMsg : join3(process.cwd(), ".git", "COMMIT_EDITMSG");
|
|
3379
|
+
await checkCommitMsg(msgFile, { verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
3380
|
+
await closePool();
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
2786
3383
|
if (opts.ci && opts.all) {
|
|
2787
3384
|
console.error(chalk2.red("\n Choose one mode: --ci or --all.\n"));
|
|
2788
3385
|
process.exit(1);
|
|
@@ -2800,7 +3397,7 @@ program.command("check").description("Check staged changes against architecture
|
|
|
2800
3397
|
if (summary.violations > 0) process.exit(1);
|
|
2801
3398
|
return;
|
|
2802
3399
|
}
|
|
2803
|
-
await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
3400
|
+
await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false });
|
|
2804
3401
|
});
|
|
2805
3402
|
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
3403
|
await phase1.providers.watchService.start({
|