stackmem 1.0.11 → 1.0.13

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 CHANGED
@@ -35,6 +35,7 @@ function usage() {
35
35
  cm inspect [project] <memory-id>
36
36
  cm compress [project]
37
37
  cm init <project> <path>
38
+ cm sync
38
39
  cm context [--project <name>] <task>
39
40
  cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
40
41
  cm doctor
@@ -105,8 +106,16 @@ function cmdHelp() {
105
106
  the stackmem MCP server with Claude Code, write a CLAUDE.md,
106
107
  .cursorrules, .windsurfrules, and .stackmem file, and seed
107
108
  initial memories from the project scan. .cursorrules and
108
- .windsurfrules (for Cursor and Windsurf) are skipped if they
109
- already exist.
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.
110
119
 
111
120
  cm context [--project <name>] <task>
112
121
  Return only the top 5 memories most relevant to a specific
@@ -673,6 +682,15 @@ const savedItems = [];
673
682
 
674
683
  const commitMessage = git(["log", "-1", "--pretty=%B"]);
675
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
+
676
694
  const isFirstCommit = (() => {
677
695
  try {
678
696
  execFileSync("git", ["rev-parse", "HEAD~1"], { stdio: "pipe" });
@@ -835,6 +853,22 @@ console.log(\`coding-memory post-commit summary (\${changedFiles.length} file(s)
835
853
  for (const item of savedItems) {
836
854
  console.log(\` - \${item}\`);
837
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
+ }
838
872
  `;
839
873
  }
840
874
  function registerMcpServer(distIndexPath) {
@@ -881,40 +915,96 @@ cm save ${project} <type> "<content>"
881
915
  nodeFs.writeFileSync(claudeMdPath, content, "utf-8");
882
916
  console.log("CLAUDE.md written.");
883
917
  }
884
- const EDITOR_RULES_CONTENT = `# stackmem
885
-
886
- At the start of every session, run:
887
- cm start
888
-
889
- and paste the output here before starting work.
890
-
891
- When you make a decision, discover a constraint,
892
- reject an approach, or make a discovery, save it:
893
- cm save <type> "<content>"
894
-
895
- Types: decision, rejection, constraint, discovery
896
- `;
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
+ }
897
987
  // Returns true if the file was written, false if it already existed
898
988
  // (and was skipped).
899
- function writeCursorRules(resolvedPath) {
989
+ function writeCursorRules(resolvedPath, content) {
900
990
  const cursorRulesPath = nodePath.join(resolvedPath, ".cursorrules");
901
991
  if (nodeFs.existsSync(cursorRulesPath)) {
902
992
  console.log(".cursorrules already exists — skipping.");
903
993
  return false;
904
994
  }
905
- nodeFs.writeFileSync(cursorRulesPath, EDITOR_RULES_CONTENT, "utf-8");
995
+ nodeFs.writeFileSync(cursorRulesPath, content, "utf-8");
906
996
  console.log(".cursorrules written.");
907
997
  return true;
908
998
  }
909
999
  // Returns true if the file was written, false if it already existed
910
1000
  // (and was skipped).
911
- function writeWindsurfRules(resolvedPath) {
1001
+ function writeWindsurfRules(resolvedPath, content) {
912
1002
  const windsurfRulesPath = nodePath.join(resolvedPath, ".windsurfrules");
913
1003
  if (nodeFs.existsSync(windsurfRulesPath)) {
914
1004
  console.log(".windsurfrules already exists — skipping.");
915
1005
  return false;
916
1006
  }
917
- nodeFs.writeFileSync(windsurfRulesPath, EDITOR_RULES_CONTENT, "utf-8");
1007
+ nodeFs.writeFileSync(windsurfRulesPath, content, "utf-8");
918
1008
  console.log(".windsurfrules written.");
919
1009
  return true;
920
1010
  }
@@ -1016,10 +1106,11 @@ async function cmdInit(project, targetPath) {
1016
1106
  console.log(`coding-memory hook installed for project ${project} at ${targetPath}`);
1017
1107
  registerMcpServer(DIST_INDEX_PATH);
1018
1108
  writeClaudeMd(project, resolvedPath);
1019
- const cursorRulesWritten = writeCursorRules(resolvedPath);
1020
- const windsurfRulesWritten = writeWindsurfRules(resolvedPath);
1021
1109
  const seededCount = await seedMemories(project, resolvedPath);
1022
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);
1023
1114
  const backendOk = await verifyBackendConnection(project);
1024
1115
  console.log("");
1025
1116
  console.log("✓ Git hook installed");
@@ -1041,6 +1132,14 @@ async function cmdInit(project, targetPath) {
1041
1132
  // .stackmem pointed to before this run, not the one being initialized now.
1042
1133
  writeProjectFile(project, resolvedPath);
1043
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
+ }
1044
1143
  // --- cm context ------------------------------------------------------------
1045
1144
  const CONTEXT_TYPE_ORDER = ["decision", "constraint", "rejection", "discovery"];
1046
1145
  const CONTEXT_TOP_N = 5;
@@ -1530,6 +1629,13 @@ async function main() {
1530
1629
  await cmdInit(project, targetPath);
1531
1630
  break;
1532
1631
  }
1632
+ case "sync": {
1633
+ if (args.length > 0)
1634
+ usage();
1635
+ const project = requireProject(null);
1636
+ await cmdSync(project);
1637
+ break;
1638
+ }
1533
1639
  case "context": {
1534
1640
  const split = splitProjectFromVariadic(args);
1535
1641
  const project = requireProject(split.project);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackmem",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -54,6 +54,7 @@ function usage(): never {
54
54
  cm inspect [project] <memory-id>
55
55
  cm compress [project]
56
56
  cm init <project> <path>
57
+ cm sync
57
58
  cm context [--project <name>] <task>
58
59
  cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
59
60
  cm doctor
@@ -125,8 +126,16 @@ function cmdHelp() {
125
126
  the stackmem MCP server with Claude Code, write a CLAUDE.md,
126
127
  .cursorrules, .windsurfrules, and .stackmem file, and seed
127
128
  initial memories from the project scan. .cursorrules and
128
- .windsurfrules (for Cursor and Windsurf) are skipped if they
129
- already exist.
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.
130
139
 
131
140
  cm context [--project <name>] <task>
132
141
  Return only the top 5 memories most relevant to a specific
@@ -793,6 +802,15 @@ const savedItems = [];
793
802
 
794
803
  const commitMessage = git(["log", "-1", "--pretty=%B"]);
795
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
+
796
814
  const isFirstCommit = (() => {
797
815
  try {
798
816
  execFileSync("git", ["rev-parse", "HEAD~1"], { stdio: "pipe" });
@@ -955,6 +973,22 @@ console.log(\`coding-memory post-commit summary (\${changedFiles.length} file(s)
955
973
  for (const item of savedItems) {
956
974
  console.log(\` - \${item}\`);
957
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
+ }
958
992
  `;
959
993
  }
960
994
 
@@ -1011,23 +1045,88 @@ cm save ${project} <type> "<content>"
1011
1045
  console.log("CLAUDE.md written.");
1012
1046
  }
1013
1047
 
1014
- const EDITOR_RULES_CONTENT = `# stackmem
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);
1015
1063
 
1016
- At the start of every session, run:
1017
- cm start
1064
+ if (memoriesError) fail(`Failed to load memories: ${memoriesError.message}`);
1018
1065
 
1019
- and paste the output here before starting work.
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);
1020
1072
 
1021
- When you make a decision, discover a constraint,
1022
- reject an approach, or make a discovery, save it:
1023
- cm save <type> "<content>"
1073
+ const budgetedMemories = applyContextBudget(scoredMemories, CONTEXT_BUDGET_TOKENS);
1024
1074
 
1025
- Types: decision, rejection, constraint, discovery
1026
- `;
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
+ }
1027
1126
 
1028
1127
  // Returns true if the file was written, false if it already existed
1029
1128
  // (and was skipped).
1030
- function writeCursorRules(resolvedPath: string): boolean {
1129
+ function writeCursorRules(resolvedPath: string, content: string): boolean {
1031
1130
  const cursorRulesPath = nodePath.join(resolvedPath, ".cursorrules");
1032
1131
 
1033
1132
  if (nodeFs.existsSync(cursorRulesPath)) {
@@ -1035,14 +1134,14 @@ function writeCursorRules(resolvedPath: string): boolean {
1035
1134
  return false;
1036
1135
  }
1037
1136
 
1038
- nodeFs.writeFileSync(cursorRulesPath, EDITOR_RULES_CONTENT, "utf-8");
1137
+ nodeFs.writeFileSync(cursorRulesPath, content, "utf-8");
1039
1138
  console.log(".cursorrules written.");
1040
1139
  return true;
1041
1140
  }
1042
1141
 
1043
1142
  // Returns true if the file was written, false if it already existed
1044
1143
  // (and was skipped).
1045
- function writeWindsurfRules(resolvedPath: string): boolean {
1144
+ function writeWindsurfRules(resolvedPath: string, content: string): boolean {
1046
1145
  const windsurfRulesPath = nodePath.join(resolvedPath, ".windsurfrules");
1047
1146
 
1048
1147
  if (nodeFs.existsSync(windsurfRulesPath)) {
@@ -1050,7 +1149,7 @@ function writeWindsurfRules(resolvedPath: string): boolean {
1050
1149
  return false;
1051
1150
  }
1052
1151
 
1053
- nodeFs.writeFileSync(windsurfRulesPath, EDITOR_RULES_CONTENT, "utf-8");
1152
+ nodeFs.writeFileSync(windsurfRulesPath, content, "utf-8");
1054
1153
  console.log(".windsurfrules written.");
1055
1154
  return true;
1056
1155
  }
@@ -1168,12 +1267,14 @@ async function cmdInit(project: string, targetPath: string) {
1168
1267
 
1169
1268
  registerMcpServer(DIST_INDEX_PATH);
1170
1269
  writeClaudeMd(project, resolvedPath);
1171
- const cursorRulesWritten = writeCursorRules(resolvedPath);
1172
- const windsurfRulesWritten = writeWindsurfRules(resolvedPath);
1173
1270
 
1174
1271
  const seededCount = await seedMemories(project, resolvedPath);
1175
1272
  console.log(`Seeded ${seededCount} initial memories from project scan.`);
1176
1273
 
1274
+ const rulesContent = await generateRulesContent(project);
1275
+ const cursorRulesWritten = writeCursorRules(resolvedPath, rulesContent);
1276
+ const windsurfRulesWritten = writeWindsurfRules(resolvedPath, rulesContent);
1277
+
1177
1278
  const backendOk = await verifyBackendConnection(project);
1178
1279
 
1179
1280
  console.log("");
@@ -1203,6 +1304,18 @@ async function cmdInit(project: string, targetPath: string) {
1203
1304
  writeProjectFile(project, resolvedPath);
1204
1305
  }
1205
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
+
1206
1319
  // --- cm context ------------------------------------------------------------
1207
1320
 
1208
1321
  const CONTEXT_TYPE_ORDER: MemoryType[] = ["decision", "constraint", "rejection", "discovery"];
@@ -1731,6 +1844,12 @@ async function main() {
1731
1844
  await cmdInit(project, targetPath);
1732
1845
  break;
1733
1846
  }
1847
+ case "sync": {
1848
+ if (args.length > 0) usage();
1849
+ const project = requireProject(null);
1850
+ await cmdSync(project);
1851
+ break;
1852
+ }
1734
1853
  case "context": {
1735
1854
  const split = splitProjectFromVariadic(args);
1736
1855
  const project = requireProject(split.project);
package/src/tools.ts CHANGED
@@ -0,0 +1 @@
1
+ const test = 1