stackmem 1.0.10 → 1.0.12
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/dist/cli.js +225 -22
- package/package.json +1 -1
- package/src/cli.ts +254 -17
- package/src/tools.ts +1 -0
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { analyzeCodebase } from "./ast-index.js";
|
|
8
8
|
import { getDeviceId } from "./device.js";
|
|
9
|
-
import { applyContextBudget, buildSummaryContent, clusterMemories, computeDecayScore, computeMemoryLinks, decideSaveAction, estimateTokens, indicatesFailedApproach, isAuthRelatedFile, isCodeFile, isQualityMessage, scoreMemoryRelevance, } from "./scoring.js";
|
|
9
|
+
import { applyContextBudget, buildSummaryContent, clusterMemories, computeDecayScore, computeMemoryLinks, decideSaveAction, estimateTokens, extractEntities, indicatesFailedApproach, isAuthRelatedFile, isCodeFile, isQualityMessage, scoreMemoryRelevance, } from "./scoring.js";
|
|
10
10
|
import { supabase } from "./storage.js";
|
|
11
11
|
const CONTEXT_BUDGET_TOKENS = 2000;
|
|
12
12
|
const COMPRESSION_THRESHOLD = 20;
|
|
@@ -32,8 +32,10 @@ function usage() {
|
|
|
32
32
|
cm search [--project <name>] <query>
|
|
33
33
|
cm resolve [project] <memory-id>
|
|
34
34
|
cm delete [project] <memory-id>
|
|
35
|
+
cm inspect [project] <memory-id>
|
|
35
36
|
cm compress [project]
|
|
36
37
|
cm init <project> <path>
|
|
38
|
+
cm sync
|
|
37
39
|
cm context [--project <name>] <task>
|
|
38
40
|
cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
|
|
39
41
|
cm doctor
|
|
@@ -88,6 +90,13 @@ function cmdHelp() {
|
|
|
88
90
|
cm delete [project] <memory-id>
|
|
89
91
|
Delete a memory and any memory_links referencing it.
|
|
90
92
|
|
|
93
|
+
cm inspect [project] <memory-id>
|
|
94
|
+
Show a rich view of a single memory: type, content, created
|
|
95
|
+
date, access count, decay score, and resolved status, plus
|
|
96
|
+
every linked memory (in either link direction) with its link
|
|
97
|
+
score, and any execution_log fixes whose problem or solution
|
|
98
|
+
shares a key entity with the memory's content.
|
|
99
|
+
|
|
91
100
|
cm compress [project]
|
|
92
101
|
Cluster related unresolved memories (needs 20+) and collapse
|
|
93
102
|
clusters of 3 or more into a single summary memory.
|
|
@@ -97,8 +106,16 @@ function cmdHelp() {
|
|
|
97
106
|
the stackmem MCP server with Claude Code, write a CLAUDE.md,
|
|
98
107
|
.cursorrules, .windsurfrules, and .stackmem file, and seed
|
|
99
108
|
initial memories from the project scan. .cursorrules and
|
|
100
|
-
.windsurfrules
|
|
101
|
-
already
|
|
109
|
+
.windsurfrules are pre-filled with the project's current
|
|
110
|
+
memories (for Cursor and Windsurf) and skipped if they already
|
|
111
|
+
exist. The post-commit hook keeps them fresh afterward by
|
|
112
|
+
running 'cm sync' on every commit.
|
|
113
|
+
|
|
114
|
+
cm sync
|
|
115
|
+
Regenerate .cursorrules and .windsurfrules from the project's
|
|
116
|
+
current unresolved memories (decay-ranked, capped at a 2000
|
|
117
|
+
token budget) and its 5 most recent open execution_log fixes,
|
|
118
|
+
overwriting both files. Project is read from .stackmem.
|
|
102
119
|
|
|
103
120
|
cm context [--project <name>] <task>
|
|
104
121
|
Return only the top 5 memories most relevant to a specific
|
|
@@ -496,6 +513,86 @@ async function cmdDelete(project, memoryId) {
|
|
|
496
513
|
fail(`Failed to delete memory: ${error.message}`);
|
|
497
514
|
console.log(`Deleted memory [${memoryId}]`);
|
|
498
515
|
}
|
|
516
|
+
// --- cm inspect ------------------------------------------------------------
|
|
517
|
+
function formatDate(dateString) {
|
|
518
|
+
return new Date(dateString).toLocaleDateString("en-US", {
|
|
519
|
+
month: "short",
|
|
520
|
+
day: "numeric",
|
|
521
|
+
year: "numeric",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
async function cmdInspect(project, memoryId) {
|
|
525
|
+
const { data: memory, error } = await supabase
|
|
526
|
+
.from("memories")
|
|
527
|
+
.select("*")
|
|
528
|
+
.eq("project", project)
|
|
529
|
+
.eq("id", memoryId)
|
|
530
|
+
.single();
|
|
531
|
+
if (error || !memory)
|
|
532
|
+
fail(`Memory [${memoryId}] not found for "${project}".`);
|
|
533
|
+
const decayScore = computeDecayScore(new Date(memory.created_at), memory.access_count ?? 0);
|
|
534
|
+
const { data: links, error: linksError } = await supabase
|
|
535
|
+
.from("memory_links")
|
|
536
|
+
.select("source_id, target_id, score")
|
|
537
|
+
.or(`source_id.eq.${memoryId},target_id.eq.${memoryId}`);
|
|
538
|
+
if (linksError)
|
|
539
|
+
fail(`Failed to load memory links: ${linksError.message}`);
|
|
540
|
+
const linkedEntries = (links ?? []).map((link) => ({
|
|
541
|
+
linkedId: link.source_id === memoryId ? link.target_id : link.source_id,
|
|
542
|
+
score: link.score,
|
|
543
|
+
}));
|
|
544
|
+
let linkedMemories = [];
|
|
545
|
+
if (linkedEntries.length > 0) {
|
|
546
|
+
const linkedIds = linkedEntries.map((entry) => entry.linkedId);
|
|
547
|
+
const { data: linkedRows, error: linkedError } = await supabase
|
|
548
|
+
.from("memories")
|
|
549
|
+
.select("id, type, content, created_at")
|
|
550
|
+
.in("id", linkedIds);
|
|
551
|
+
if (linkedError)
|
|
552
|
+
fail(`Failed to load linked memories: ${linkedError.message}`);
|
|
553
|
+
const scoreById = new Map(linkedEntries.map((entry) => [entry.linkedId, entry.score]));
|
|
554
|
+
linkedMemories = (linkedRows ?? [])
|
|
555
|
+
.map((row) => ({ ...row, score: scoreById.get(row.id) ?? 0 }))
|
|
556
|
+
.sort((a, b) => b.score - a.score);
|
|
557
|
+
}
|
|
558
|
+
const entities = extractEntities(memory.content);
|
|
559
|
+
const { data: fixes, error: fixesError } = await supabase
|
|
560
|
+
.from("execution_log")
|
|
561
|
+
.select("problem, solution")
|
|
562
|
+
.eq("project", project);
|
|
563
|
+
if (fixesError)
|
|
564
|
+
fail(`Failed to load execution log: ${fixesError.message}`);
|
|
565
|
+
const relatedFixes = (fixes ?? []).filter((fix) => {
|
|
566
|
+
const fixEntities = extractEntities(`${fix.problem} ${fix.solution}`);
|
|
567
|
+
for (const entity of entities) {
|
|
568
|
+
if (fixEntities.has(entity))
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
return false;
|
|
572
|
+
});
|
|
573
|
+
const divider = "─".repeat(40);
|
|
574
|
+
console.log(divider);
|
|
575
|
+
console.log(`${memory.type.toUpperCase()} · accessed ${memory.access_count ?? 0}x · decay ${decayScore.toFixed(2)}`);
|
|
576
|
+
console.log(divider);
|
|
577
|
+
console.log(memory.content);
|
|
578
|
+
console.log("");
|
|
579
|
+
console.log(`Created: ${formatDate(memory.created_at)}`);
|
|
580
|
+
console.log(`Status: ${memory.resolved ? "resolved" : "unresolved"}`);
|
|
581
|
+
console.log("");
|
|
582
|
+
console.log(`LINKED MEMORIES (${linkedMemories.length})`);
|
|
583
|
+
for (const linked of linkedMemories) {
|
|
584
|
+
console.log(` └─ [score: ${linked.score.toFixed(2)}] ${linked.type.toUpperCase()}`);
|
|
585
|
+
console.log(` ${linked.content}`);
|
|
586
|
+
console.log(` ${formatDate(linked.created_at)}`);
|
|
587
|
+
console.log("");
|
|
588
|
+
}
|
|
589
|
+
if (relatedFixes.length > 0) {
|
|
590
|
+
console.log(`RELATED FIXES (${relatedFixes.length})`);
|
|
591
|
+
for (const fix of relatedFixes) {
|
|
592
|
+
console.log(` └─ FIX: ${fix.problem} → ${fix.solution}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
499
596
|
// --- cm init -------------------------------------------------------------
|
|
500
597
|
// The hook logic lives in its own .mjs file (not directly in
|
|
501
598
|
// .git/hooks/post-commit) so its module type is unambiguous — an
|
|
@@ -585,6 +682,15 @@ const savedItems = [];
|
|
|
585
682
|
|
|
586
683
|
const commitMessage = git(["log", "-1", "--pretty=%B"]);
|
|
587
684
|
|
|
685
|
+
// The sync commit below is itself a commit, so it fires this same
|
|
686
|
+
// post-commit hook again — git's --no-verify only skips pre-commit and
|
|
687
|
+
// commit-msg hooks, never post-commit. Without this guard, the hook would
|
|
688
|
+
// save its own commit message as a new decision memory, which changes the
|
|
689
|
+
// rules-file content and triggers another sync commit, indefinitely.
|
|
690
|
+
if (commitMessage.includes("[skip ci]")) {
|
|
691
|
+
process.exit(0);
|
|
692
|
+
}
|
|
693
|
+
|
|
588
694
|
const isFirstCommit = (() => {
|
|
589
695
|
try {
|
|
590
696
|
execFileSync("git", ["rev-parse", "HEAD~1"], { stdio: "pipe" });
|
|
@@ -747,6 +853,22 @@ console.log(\`coding-memory post-commit summary (\${changedFiles.length} file(s)
|
|
|
747
853
|
for (const item of savedItems) {
|
|
748
854
|
console.log(\` - \${item}\`);
|
|
749
855
|
}
|
|
856
|
+
|
|
857
|
+
execFileSync("node", [CLI_PATH, "sync"], { stdio: "inherit" });
|
|
858
|
+
|
|
859
|
+
// cm sync rewrites .cursorrules/.windsurfrules, which would otherwise leave
|
|
860
|
+
// the working directory dirty (breaking things like \`npm version patch\`
|
|
861
|
+
// that require a clean tree). Commit the sync ourselves instead. --no-verify
|
|
862
|
+
// skips pre-commit/commit-msg hooks (not post-commit — see the [skip ci]
|
|
863
|
+
// guard above, which is what actually stops this from recursing).
|
|
864
|
+
try {
|
|
865
|
+
execFileSync("git", ["add", ".cursorrules", ".windsurfrules"], { stdio: "pipe" });
|
|
866
|
+
execFileSync("git", ["commit", "--no-verify", "-m", "chore: sync memory to rules files [skip ci]"], {
|
|
867
|
+
stdio: "pipe",
|
|
868
|
+
});
|
|
869
|
+
} catch {
|
|
870
|
+
// Nothing to commit (rules files unchanged) — fine, ignore.
|
|
871
|
+
}
|
|
750
872
|
`;
|
|
751
873
|
}
|
|
752
874
|
function registerMcpServer(distIndexPath) {
|
|
@@ -793,40 +915,96 @@ cm save ${project} <type> "<content>"
|
|
|
793
915
|
nodeFs.writeFileSync(claudeMdPath, content, "utf-8");
|
|
794
916
|
console.log("CLAUDE.md written.");
|
|
795
917
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
918
|
+
// Section order for the generated rules-file markdown — matches the order
|
|
919
|
+
// memories are presented in everywhere else (cm context's CONTEXT_TYPE_ORDER),
|
|
920
|
+
// not MEMORY_TYPES' declaration order.
|
|
921
|
+
const RULES_FILE_TYPE_ORDER = ["decision", "constraint", "rejection", "discovery"];
|
|
922
|
+
// Builds the markdown written into .cursorrules / .windsurfrules: the
|
|
923
|
+
// project's unresolved memories (decay-ranked, budget-capped so an
|
|
924
|
+
// editor's system prompt never balloons) plus its most recent open fixes.
|
|
925
|
+
// Shared by `cm init` (first write) and `cm sync` (refresh on every commit).
|
|
926
|
+
async function generateRulesContent(project) {
|
|
927
|
+
const { data: memoryRows, error: memoriesError } = await supabase
|
|
928
|
+
.from("memories")
|
|
929
|
+
.select("*")
|
|
930
|
+
.eq("project", project)
|
|
931
|
+
.eq("resolved", false);
|
|
932
|
+
if (memoriesError)
|
|
933
|
+
fail(`Failed to load memories: ${memoriesError.message}`);
|
|
934
|
+
const scoredMemories = (memoryRows ?? [])
|
|
935
|
+
.map((memory) => ({
|
|
936
|
+
...memory,
|
|
937
|
+
decay_score: computeDecayScore(new Date(memory.created_at), memory.access_count ?? 0),
|
|
938
|
+
}))
|
|
939
|
+
.sort((a, b) => b.decay_score - a.decay_score);
|
|
940
|
+
const budgetedMemories = applyContextBudget(scoredMemories, CONTEXT_BUDGET_TOKENS);
|
|
941
|
+
const { data: fixRows, error: fixesError } = await supabase
|
|
942
|
+
.from("execution_log")
|
|
943
|
+
.select("*")
|
|
944
|
+
.eq("project", project)
|
|
945
|
+
.eq("resolved", false)
|
|
946
|
+
.order("created_at", { ascending: false })
|
|
947
|
+
.limit(5);
|
|
948
|
+
if (fixesError)
|
|
949
|
+
fail(`Failed to load execution log: ${fixesError.message}`);
|
|
950
|
+
const fixes = fixRows ?? [];
|
|
951
|
+
const lines = [];
|
|
952
|
+
lines.push("# stackmem — project memory (auto-updated)");
|
|
953
|
+
lines.push(`## Project: ${project}`);
|
|
954
|
+
if (budgetedMemories.length === 0 && fixes.length === 0) {
|
|
955
|
+
lines.push("");
|
|
956
|
+
lines.push("No memories yet. Make a commit to start capturing.");
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
for (const type of RULES_FILE_TYPE_ORDER) {
|
|
960
|
+
const group = budgetedMemories.filter((memory) => memory.type === type);
|
|
961
|
+
if (group.length === 0)
|
|
962
|
+
continue;
|
|
963
|
+
lines.push("");
|
|
964
|
+
lines.push(`### ${TYPE_LABELS[type]}`);
|
|
965
|
+
for (const memory of group) {
|
|
966
|
+
lines.push(`- ${memory.content}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (fixes.length > 0) {
|
|
970
|
+
lines.push("");
|
|
971
|
+
lines.push("### Recent Fixes");
|
|
972
|
+
for (const fix of fixes) {
|
|
973
|
+
const solution = fix.solution && fix.solution.trim().length > 0
|
|
974
|
+
? fix.solution
|
|
975
|
+
: "(solution pending)";
|
|
976
|
+
lines.push(`- ${fix.problem} → ${solution}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
lines.push("");
|
|
981
|
+
lines.push("---");
|
|
982
|
+
lines.push(`*Last updated: ${new Date().toLocaleString("en-US")}*`);
|
|
983
|
+
lines.push("*Run `cm start` for full details with decay scores*");
|
|
984
|
+
lines.push("");
|
|
985
|
+
return lines.join("\n");
|
|
986
|
+
}
|
|
809
987
|
// Returns true if the file was written, false if it already existed
|
|
810
988
|
// (and was skipped).
|
|
811
|
-
function writeCursorRules(resolvedPath) {
|
|
989
|
+
function writeCursorRules(resolvedPath, content) {
|
|
812
990
|
const cursorRulesPath = nodePath.join(resolvedPath, ".cursorrules");
|
|
813
991
|
if (nodeFs.existsSync(cursorRulesPath)) {
|
|
814
992
|
console.log(".cursorrules already exists — skipping.");
|
|
815
993
|
return false;
|
|
816
994
|
}
|
|
817
|
-
nodeFs.writeFileSync(cursorRulesPath,
|
|
995
|
+
nodeFs.writeFileSync(cursorRulesPath, content, "utf-8");
|
|
818
996
|
console.log(".cursorrules written.");
|
|
819
997
|
return true;
|
|
820
998
|
}
|
|
821
999
|
// Returns true if the file was written, false if it already existed
|
|
822
1000
|
// (and was skipped).
|
|
823
|
-
function writeWindsurfRules(resolvedPath) {
|
|
1001
|
+
function writeWindsurfRules(resolvedPath, content) {
|
|
824
1002
|
const windsurfRulesPath = nodePath.join(resolvedPath, ".windsurfrules");
|
|
825
1003
|
if (nodeFs.existsSync(windsurfRulesPath)) {
|
|
826
1004
|
console.log(".windsurfrules already exists — skipping.");
|
|
827
1005
|
return false;
|
|
828
1006
|
}
|
|
829
|
-
nodeFs.writeFileSync(windsurfRulesPath,
|
|
1007
|
+
nodeFs.writeFileSync(windsurfRulesPath, content, "utf-8");
|
|
830
1008
|
console.log(".windsurfrules written.");
|
|
831
1009
|
return true;
|
|
832
1010
|
}
|
|
@@ -928,10 +1106,11 @@ async function cmdInit(project, targetPath) {
|
|
|
928
1106
|
console.log(`coding-memory hook installed for project ${project} at ${targetPath}`);
|
|
929
1107
|
registerMcpServer(DIST_INDEX_PATH);
|
|
930
1108
|
writeClaudeMd(project, resolvedPath);
|
|
931
|
-
const cursorRulesWritten = writeCursorRules(resolvedPath);
|
|
932
|
-
const windsurfRulesWritten = writeWindsurfRules(resolvedPath);
|
|
933
1109
|
const seededCount = await seedMemories(project, resolvedPath);
|
|
934
1110
|
console.log(`Seeded ${seededCount} initial memories from project scan.`);
|
|
1111
|
+
const rulesContent = await generateRulesContent(project);
|
|
1112
|
+
const cursorRulesWritten = writeCursorRules(resolvedPath, rulesContent);
|
|
1113
|
+
const windsurfRulesWritten = writeWindsurfRules(resolvedPath, rulesContent);
|
|
935
1114
|
const backendOk = await verifyBackendConnection(project);
|
|
936
1115
|
console.log("");
|
|
937
1116
|
console.log("✓ Git hook installed");
|
|
@@ -953,6 +1132,14 @@ async function cmdInit(project, targetPath) {
|
|
|
953
1132
|
// .stackmem pointed to before this run, not the one being initialized now.
|
|
954
1133
|
writeProjectFile(project, resolvedPath);
|
|
955
1134
|
}
|
|
1135
|
+
// --- cm sync ---------------------------------------------------------------
|
|
1136
|
+
async function cmdSync(project) {
|
|
1137
|
+
const resolvedPath = process.cwd();
|
|
1138
|
+
const rulesContent = await generateRulesContent(project);
|
|
1139
|
+
nodeFs.writeFileSync(nodePath.join(resolvedPath, ".cursorrules"), rulesContent, "utf-8");
|
|
1140
|
+
nodeFs.writeFileSync(nodePath.join(resolvedPath, ".windsurfrules"), rulesContent, "utf-8");
|
|
1141
|
+
console.log("Synced memories to .cursorrules and .windsurfrules");
|
|
1142
|
+
}
|
|
956
1143
|
// --- cm context ------------------------------------------------------------
|
|
957
1144
|
const CONTEXT_TYPE_ORDER = ["decision", "constraint", "rejection", "discovery"];
|
|
958
1145
|
const CONTEXT_TOP_N = 5;
|
|
@@ -1418,6 +1605,15 @@ async function main() {
|
|
|
1418
1605
|
await cmdDelete(project, memoryId);
|
|
1419
1606
|
break;
|
|
1420
1607
|
}
|
|
1608
|
+
case "inspect": {
|
|
1609
|
+
const split = splitOptionalProject(args, 1);
|
|
1610
|
+
if (!split)
|
|
1611
|
+
usage();
|
|
1612
|
+
const project = requireProject(split.project);
|
|
1613
|
+
const [memoryId] = split.rest;
|
|
1614
|
+
await cmdInspect(project, memoryId);
|
|
1615
|
+
break;
|
|
1616
|
+
}
|
|
1421
1617
|
case "compress": {
|
|
1422
1618
|
const split = splitOptionalProject(args, 0);
|
|
1423
1619
|
if (!split)
|
|
@@ -1433,6 +1629,13 @@ async function main() {
|
|
|
1433
1629
|
await cmdInit(project, targetPath);
|
|
1434
1630
|
break;
|
|
1435
1631
|
}
|
|
1632
|
+
case "sync": {
|
|
1633
|
+
if (args.length > 0)
|
|
1634
|
+
usage();
|
|
1635
|
+
const project = requireProject(null);
|
|
1636
|
+
await cmdSync(project);
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1436
1639
|
case "context": {
|
|
1437
1640
|
const split = splitProjectFromVariadic(args);
|
|
1438
1641
|
const project = requireProject(split.project);
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
computeMemoryLinks,
|
|
15
15
|
decideSaveAction,
|
|
16
16
|
estimateTokens,
|
|
17
|
+
extractEntities,
|
|
17
18
|
indicatesFailedApproach,
|
|
18
19
|
isAuthRelatedFile,
|
|
19
20
|
isCodeFile,
|
|
@@ -50,8 +51,10 @@ function usage(): never {
|
|
|
50
51
|
cm search [--project <name>] <query>
|
|
51
52
|
cm resolve [project] <memory-id>
|
|
52
53
|
cm delete [project] <memory-id>
|
|
54
|
+
cm inspect [project] <memory-id>
|
|
53
55
|
cm compress [project]
|
|
54
56
|
cm init <project> <path>
|
|
57
|
+
cm sync
|
|
55
58
|
cm context [--project <name>] <task>
|
|
56
59
|
cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
|
|
57
60
|
cm doctor
|
|
@@ -107,6 +110,13 @@ function cmdHelp() {
|
|
|
107
110
|
cm delete [project] <memory-id>
|
|
108
111
|
Delete a memory and any memory_links referencing it.
|
|
109
112
|
|
|
113
|
+
cm inspect [project] <memory-id>
|
|
114
|
+
Show a rich view of a single memory: type, content, created
|
|
115
|
+
date, access count, decay score, and resolved status, plus
|
|
116
|
+
every linked memory (in either link direction) with its link
|
|
117
|
+
score, and any execution_log fixes whose problem or solution
|
|
118
|
+
shares a key entity with the memory's content.
|
|
119
|
+
|
|
110
120
|
cm compress [project]
|
|
111
121
|
Cluster related unresolved memories (needs 20+) and collapse
|
|
112
122
|
clusters of 3 or more into a single summary memory.
|
|
@@ -116,8 +126,16 @@ function cmdHelp() {
|
|
|
116
126
|
the stackmem MCP server with Claude Code, write a CLAUDE.md,
|
|
117
127
|
.cursorrules, .windsurfrules, and .stackmem file, and seed
|
|
118
128
|
initial memories from the project scan. .cursorrules and
|
|
119
|
-
.windsurfrules
|
|
120
|
-
already
|
|
129
|
+
.windsurfrules are pre-filled with the project's current
|
|
130
|
+
memories (for Cursor and Windsurf) and skipped if they already
|
|
131
|
+
exist. The post-commit hook keeps them fresh afterward by
|
|
132
|
+
running 'cm sync' on every commit.
|
|
133
|
+
|
|
134
|
+
cm sync
|
|
135
|
+
Regenerate .cursorrules and .windsurfrules from the project's
|
|
136
|
+
current unresolved memories (decay-ranked, capped at a 2000
|
|
137
|
+
token budget) and its 5 most recent open execution_log fixes,
|
|
138
|
+
overwriting both files. Project is read from .stackmem.
|
|
121
139
|
|
|
122
140
|
cm context [--project <name>] <task>
|
|
123
141
|
Return only the top 5 memories most relevant to a specific
|
|
@@ -593,6 +611,107 @@ async function cmdDelete(project: string, memoryId: string) {
|
|
|
593
611
|
console.log(`Deleted memory [${memoryId}]`);
|
|
594
612
|
}
|
|
595
613
|
|
|
614
|
+
// --- cm inspect ------------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
function formatDate(dateString: string): string {
|
|
617
|
+
return new Date(dateString).toLocaleDateString("en-US", {
|
|
618
|
+
month: "short",
|
|
619
|
+
day: "numeric",
|
|
620
|
+
year: "numeric",
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function cmdInspect(project: string, memoryId: string) {
|
|
625
|
+
const { data: memory, error } = await supabase
|
|
626
|
+
.from("memories")
|
|
627
|
+
.select("*")
|
|
628
|
+
.eq("project", project)
|
|
629
|
+
.eq("id", memoryId)
|
|
630
|
+
.single();
|
|
631
|
+
|
|
632
|
+
if (error || !memory) fail(`Memory [${memoryId}] not found for "${project}".`);
|
|
633
|
+
|
|
634
|
+
const decayScore = computeDecayScore(new Date(memory.created_at), memory.access_count ?? 0);
|
|
635
|
+
|
|
636
|
+
const { data: links, error: linksError } = await supabase
|
|
637
|
+
.from("memory_links")
|
|
638
|
+
.select("source_id, target_id, score")
|
|
639
|
+
.or(`source_id.eq.${memoryId},target_id.eq.${memoryId}`);
|
|
640
|
+
|
|
641
|
+
if (linksError) fail(`Failed to load memory links: ${linksError.message}`);
|
|
642
|
+
|
|
643
|
+
const linkedEntries = (links ?? []).map((link) => ({
|
|
644
|
+
linkedId: link.source_id === memoryId ? link.target_id : link.source_id,
|
|
645
|
+
score: link.score,
|
|
646
|
+
}));
|
|
647
|
+
|
|
648
|
+
let linkedMemories: Array<{
|
|
649
|
+
id: string;
|
|
650
|
+
type: string;
|
|
651
|
+
content: string;
|
|
652
|
+
created_at: string;
|
|
653
|
+
score: number;
|
|
654
|
+
}> = [];
|
|
655
|
+
|
|
656
|
+
if (linkedEntries.length > 0) {
|
|
657
|
+
const linkedIds = linkedEntries.map((entry) => entry.linkedId);
|
|
658
|
+
const { data: linkedRows, error: linkedError } = await supabase
|
|
659
|
+
.from("memories")
|
|
660
|
+
.select("id, type, content, created_at")
|
|
661
|
+
.in("id", linkedIds);
|
|
662
|
+
|
|
663
|
+
if (linkedError) fail(`Failed to load linked memories: ${linkedError.message}`);
|
|
664
|
+
|
|
665
|
+
const scoreById = new Map(linkedEntries.map((entry) => [entry.linkedId, entry.score]));
|
|
666
|
+
linkedMemories = (linkedRows ?? [])
|
|
667
|
+
.map((row) => ({ ...row, score: scoreById.get(row.id) ?? 0 }))
|
|
668
|
+
.sort((a, b) => b.score - a.score);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const entities = extractEntities(memory.content);
|
|
672
|
+
const { data: fixes, error: fixesError } = await supabase
|
|
673
|
+
.from("execution_log")
|
|
674
|
+
.select("problem, solution")
|
|
675
|
+
.eq("project", project);
|
|
676
|
+
|
|
677
|
+
if (fixesError) fail(`Failed to load execution log: ${fixesError.message}`);
|
|
678
|
+
|
|
679
|
+
const relatedFixes = (fixes ?? []).filter((fix) => {
|
|
680
|
+
const fixEntities = extractEntities(`${fix.problem} ${fix.solution}`);
|
|
681
|
+
for (const entity of entities) {
|
|
682
|
+
if (fixEntities.has(entity)) return true;
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const divider = "─".repeat(40);
|
|
688
|
+
console.log(divider);
|
|
689
|
+
console.log(
|
|
690
|
+
`${memory.type.toUpperCase()} · accessed ${memory.access_count ?? 0}x · decay ${decayScore.toFixed(2)}`
|
|
691
|
+
);
|
|
692
|
+
console.log(divider);
|
|
693
|
+
console.log(memory.content);
|
|
694
|
+
console.log("");
|
|
695
|
+
console.log(`Created: ${formatDate(memory.created_at)}`);
|
|
696
|
+
console.log(`Status: ${memory.resolved ? "resolved" : "unresolved"}`);
|
|
697
|
+
console.log("");
|
|
698
|
+
|
|
699
|
+
console.log(`LINKED MEMORIES (${linkedMemories.length})`);
|
|
700
|
+
for (const linked of linkedMemories) {
|
|
701
|
+
console.log(` └─ [score: ${linked.score.toFixed(2)}] ${linked.type.toUpperCase()}`);
|
|
702
|
+
console.log(` ${linked.content}`);
|
|
703
|
+
console.log(` ${formatDate(linked.created_at)}`);
|
|
704
|
+
console.log("");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (relatedFixes.length > 0) {
|
|
708
|
+
console.log(`RELATED FIXES (${relatedFixes.length})`);
|
|
709
|
+
for (const fix of relatedFixes) {
|
|
710
|
+
console.log(` └─ FIX: ${fix.problem} → ${fix.solution}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
596
715
|
// --- cm init -------------------------------------------------------------
|
|
597
716
|
|
|
598
717
|
// The hook logic lives in its own .mjs file (not directly in
|
|
@@ -683,6 +802,15 @@ const savedItems = [];
|
|
|
683
802
|
|
|
684
803
|
const commitMessage = git(["log", "-1", "--pretty=%B"]);
|
|
685
804
|
|
|
805
|
+
// The sync commit below is itself a commit, so it fires this same
|
|
806
|
+
// post-commit hook again — git's --no-verify only skips pre-commit and
|
|
807
|
+
// commit-msg hooks, never post-commit. Without this guard, the hook would
|
|
808
|
+
// save its own commit message as a new decision memory, which changes the
|
|
809
|
+
// rules-file content and triggers another sync commit, indefinitely.
|
|
810
|
+
if (commitMessage.includes("[skip ci]")) {
|
|
811
|
+
process.exit(0);
|
|
812
|
+
}
|
|
813
|
+
|
|
686
814
|
const isFirstCommit = (() => {
|
|
687
815
|
try {
|
|
688
816
|
execFileSync("git", ["rev-parse", "HEAD~1"], { stdio: "pipe" });
|
|
@@ -845,6 +973,22 @@ console.log(\`coding-memory post-commit summary (\${changedFiles.length} file(s)
|
|
|
845
973
|
for (const item of savedItems) {
|
|
846
974
|
console.log(\` - \${item}\`);
|
|
847
975
|
}
|
|
976
|
+
|
|
977
|
+
execFileSync("node", [CLI_PATH, "sync"], { stdio: "inherit" });
|
|
978
|
+
|
|
979
|
+
// cm sync rewrites .cursorrules/.windsurfrules, which would otherwise leave
|
|
980
|
+
// the working directory dirty (breaking things like \`npm version patch\`
|
|
981
|
+
// that require a clean tree). Commit the sync ourselves instead. --no-verify
|
|
982
|
+
// skips pre-commit/commit-msg hooks (not post-commit — see the [skip ci]
|
|
983
|
+
// guard above, which is what actually stops this from recursing).
|
|
984
|
+
try {
|
|
985
|
+
execFileSync("git", ["add", ".cursorrules", ".windsurfrules"], { stdio: "pipe" });
|
|
986
|
+
execFileSync("git", ["commit", "--no-verify", "-m", "chore: sync memory to rules files [skip ci]"], {
|
|
987
|
+
stdio: "pipe",
|
|
988
|
+
});
|
|
989
|
+
} catch {
|
|
990
|
+
// Nothing to commit (rules files unchanged) — fine, ignore.
|
|
991
|
+
}
|
|
848
992
|
`;
|
|
849
993
|
}
|
|
850
994
|
|
|
@@ -901,23 +1045,88 @@ cm save ${project} <type> "<content>"
|
|
|
901
1045
|
console.log("CLAUDE.md written.");
|
|
902
1046
|
}
|
|
903
1047
|
|
|
904
|
-
|
|
1048
|
+
// Section order for the generated rules-file markdown — matches the order
|
|
1049
|
+
// memories are presented in everywhere else (cm context's CONTEXT_TYPE_ORDER),
|
|
1050
|
+
// not MEMORY_TYPES' declaration order.
|
|
1051
|
+
const RULES_FILE_TYPE_ORDER: MemoryType[] = ["decision", "constraint", "rejection", "discovery"];
|
|
1052
|
+
|
|
1053
|
+
// Builds the markdown written into .cursorrules / .windsurfrules: the
|
|
1054
|
+
// project's unresolved memories (decay-ranked, budget-capped so an
|
|
1055
|
+
// editor's system prompt never balloons) plus its most recent open fixes.
|
|
1056
|
+
// Shared by `cm init` (first write) and `cm sync` (refresh on every commit).
|
|
1057
|
+
async function generateRulesContent(project: string): Promise<string> {
|
|
1058
|
+
const { data: memoryRows, error: memoriesError } = await supabase
|
|
1059
|
+
.from("memories")
|
|
1060
|
+
.select("*")
|
|
1061
|
+
.eq("project", project)
|
|
1062
|
+
.eq("resolved", false);
|
|
905
1063
|
|
|
906
|
-
|
|
907
|
-
cm start
|
|
1064
|
+
if (memoriesError) fail(`Failed to load memories: ${memoriesError.message}`);
|
|
908
1065
|
|
|
909
|
-
|
|
1066
|
+
const scoredMemories = (memoryRows ?? [])
|
|
1067
|
+
.map((memory) => ({
|
|
1068
|
+
...memory,
|
|
1069
|
+
decay_score: computeDecayScore(new Date(memory.created_at), memory.access_count ?? 0),
|
|
1070
|
+
}))
|
|
1071
|
+
.sort((a, b) => b.decay_score - a.decay_score);
|
|
910
1072
|
|
|
911
|
-
|
|
912
|
-
reject an approach, or make a discovery, save it:
|
|
913
|
-
cm save <type> "<content>"
|
|
1073
|
+
const budgetedMemories = applyContextBudget(scoredMemories, CONTEXT_BUDGET_TOKENS);
|
|
914
1074
|
|
|
915
|
-
|
|
916
|
-
|
|
1075
|
+
const { data: fixRows, error: fixesError } = await supabase
|
|
1076
|
+
.from("execution_log")
|
|
1077
|
+
.select("*")
|
|
1078
|
+
.eq("project", project)
|
|
1079
|
+
.eq("resolved", false)
|
|
1080
|
+
.order("created_at", { ascending: false })
|
|
1081
|
+
.limit(5);
|
|
1082
|
+
|
|
1083
|
+
if (fixesError) fail(`Failed to load execution log: ${fixesError.message}`);
|
|
1084
|
+
|
|
1085
|
+
const fixes = fixRows ?? [];
|
|
1086
|
+
|
|
1087
|
+
const lines: string[] = [];
|
|
1088
|
+
lines.push("# stackmem — project memory (auto-updated)");
|
|
1089
|
+
lines.push(`## Project: ${project}`);
|
|
1090
|
+
|
|
1091
|
+
if (budgetedMemories.length === 0 && fixes.length === 0) {
|
|
1092
|
+
lines.push("");
|
|
1093
|
+
lines.push("No memories yet. Make a commit to start capturing.");
|
|
1094
|
+
} else {
|
|
1095
|
+
for (const type of RULES_FILE_TYPE_ORDER) {
|
|
1096
|
+
const group = budgetedMemories.filter((memory) => memory.type === type);
|
|
1097
|
+
if (group.length === 0) continue;
|
|
1098
|
+
|
|
1099
|
+
lines.push("");
|
|
1100
|
+
lines.push(`### ${TYPE_LABELS[type]}`);
|
|
1101
|
+
for (const memory of group) {
|
|
1102
|
+
lines.push(`- ${memory.content}`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (fixes.length > 0) {
|
|
1107
|
+
lines.push("");
|
|
1108
|
+
lines.push("### Recent Fixes");
|
|
1109
|
+
for (const fix of fixes) {
|
|
1110
|
+
const solution = fix.solution && fix.solution.trim().length > 0
|
|
1111
|
+
? fix.solution
|
|
1112
|
+
: "(solution pending)";
|
|
1113
|
+
lines.push(`- ${fix.problem} → ${solution}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
lines.push("");
|
|
1119
|
+
lines.push("---");
|
|
1120
|
+
lines.push(`*Last updated: ${new Date().toLocaleString("en-US")}*`);
|
|
1121
|
+
lines.push("*Run `cm start` for full details with decay scores*");
|
|
1122
|
+
lines.push("");
|
|
1123
|
+
|
|
1124
|
+
return lines.join("\n");
|
|
1125
|
+
}
|
|
917
1126
|
|
|
918
1127
|
// Returns true if the file was written, false if it already existed
|
|
919
1128
|
// (and was skipped).
|
|
920
|
-
function writeCursorRules(resolvedPath: string): boolean {
|
|
1129
|
+
function writeCursorRules(resolvedPath: string, content: string): boolean {
|
|
921
1130
|
const cursorRulesPath = nodePath.join(resolvedPath, ".cursorrules");
|
|
922
1131
|
|
|
923
1132
|
if (nodeFs.existsSync(cursorRulesPath)) {
|
|
@@ -925,14 +1134,14 @@ function writeCursorRules(resolvedPath: string): boolean {
|
|
|
925
1134
|
return false;
|
|
926
1135
|
}
|
|
927
1136
|
|
|
928
|
-
nodeFs.writeFileSync(cursorRulesPath,
|
|
1137
|
+
nodeFs.writeFileSync(cursorRulesPath, content, "utf-8");
|
|
929
1138
|
console.log(".cursorrules written.");
|
|
930
1139
|
return true;
|
|
931
1140
|
}
|
|
932
1141
|
|
|
933
1142
|
// Returns true if the file was written, false if it already existed
|
|
934
1143
|
// (and was skipped).
|
|
935
|
-
function writeWindsurfRules(resolvedPath: string): boolean {
|
|
1144
|
+
function writeWindsurfRules(resolvedPath: string, content: string): boolean {
|
|
936
1145
|
const windsurfRulesPath = nodePath.join(resolvedPath, ".windsurfrules");
|
|
937
1146
|
|
|
938
1147
|
if (nodeFs.existsSync(windsurfRulesPath)) {
|
|
@@ -940,7 +1149,7 @@ function writeWindsurfRules(resolvedPath: string): boolean {
|
|
|
940
1149
|
return false;
|
|
941
1150
|
}
|
|
942
1151
|
|
|
943
|
-
nodeFs.writeFileSync(windsurfRulesPath,
|
|
1152
|
+
nodeFs.writeFileSync(windsurfRulesPath, content, "utf-8");
|
|
944
1153
|
console.log(".windsurfrules written.");
|
|
945
1154
|
return true;
|
|
946
1155
|
}
|
|
@@ -1058,12 +1267,14 @@ async function cmdInit(project: string, targetPath: string) {
|
|
|
1058
1267
|
|
|
1059
1268
|
registerMcpServer(DIST_INDEX_PATH);
|
|
1060
1269
|
writeClaudeMd(project, resolvedPath);
|
|
1061
|
-
const cursorRulesWritten = writeCursorRules(resolvedPath);
|
|
1062
|
-
const windsurfRulesWritten = writeWindsurfRules(resolvedPath);
|
|
1063
1270
|
|
|
1064
1271
|
const seededCount = await seedMemories(project, resolvedPath);
|
|
1065
1272
|
console.log(`Seeded ${seededCount} initial memories from project scan.`);
|
|
1066
1273
|
|
|
1274
|
+
const rulesContent = await generateRulesContent(project);
|
|
1275
|
+
const cursorRulesWritten = writeCursorRules(resolvedPath, rulesContent);
|
|
1276
|
+
const windsurfRulesWritten = writeWindsurfRules(resolvedPath, rulesContent);
|
|
1277
|
+
|
|
1067
1278
|
const backendOk = await verifyBackendConnection(project);
|
|
1068
1279
|
|
|
1069
1280
|
console.log("");
|
|
@@ -1093,6 +1304,18 @@ async function cmdInit(project: string, targetPath: string) {
|
|
|
1093
1304
|
writeProjectFile(project, resolvedPath);
|
|
1094
1305
|
}
|
|
1095
1306
|
|
|
1307
|
+
// --- cm sync ---------------------------------------------------------------
|
|
1308
|
+
|
|
1309
|
+
async function cmdSync(project: string) {
|
|
1310
|
+
const resolvedPath = process.cwd();
|
|
1311
|
+
const rulesContent = await generateRulesContent(project);
|
|
1312
|
+
|
|
1313
|
+
nodeFs.writeFileSync(nodePath.join(resolvedPath, ".cursorrules"), rulesContent, "utf-8");
|
|
1314
|
+
nodeFs.writeFileSync(nodePath.join(resolvedPath, ".windsurfrules"), rulesContent, "utf-8");
|
|
1315
|
+
|
|
1316
|
+
console.log("Synced memories to .cursorrules and .windsurfrules");
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1096
1319
|
// --- cm context ------------------------------------------------------------
|
|
1097
1320
|
|
|
1098
1321
|
const CONTEXT_TYPE_ORDER: MemoryType[] = ["decision", "constraint", "rejection", "discovery"];
|
|
@@ -1600,6 +1823,14 @@ async function main() {
|
|
|
1600
1823
|
await cmdDelete(project, memoryId);
|
|
1601
1824
|
break;
|
|
1602
1825
|
}
|
|
1826
|
+
case "inspect": {
|
|
1827
|
+
const split = splitOptionalProject(args, 1);
|
|
1828
|
+
if (!split) usage();
|
|
1829
|
+
const project = requireProject(split.project);
|
|
1830
|
+
const [memoryId] = split.rest;
|
|
1831
|
+
await cmdInspect(project, memoryId);
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1603
1834
|
case "compress": {
|
|
1604
1835
|
const split = splitOptionalProject(args, 0);
|
|
1605
1836
|
if (!split) usage();
|
|
@@ -1613,6 +1844,12 @@ async function main() {
|
|
|
1613
1844
|
await cmdInit(project, targetPath);
|
|
1614
1845
|
break;
|
|
1615
1846
|
}
|
|
1847
|
+
case "sync": {
|
|
1848
|
+
if (args.length > 0) usage();
|
|
1849
|
+
const project = requireProject(null);
|
|
1850
|
+
await cmdSync(project);
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1616
1853
|
case "context": {
|
|
1617
1854
|
const split = splitProjectFromVariadic(args);
|
|
1618
1855
|
const project = requireProject(split.project);
|
package/src/tools.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const test = 1
|