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 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 (for Cursor and Windsurf) are skipped if they
101
- 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.
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
- const EDITOR_RULES_CONTENT = `# stackmem
797
-
798
- At the start of every session, run:
799
- cm start
800
-
801
- and paste the output here before starting work.
802
-
803
- When you make a decision, discover a constraint,
804
- reject an approach, or make a discovery, save it:
805
- cm save <type> "<content>"
806
-
807
- Types: decision, rejection, constraint, discovery
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, EDITOR_RULES_CONTENT, "utf-8");
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, EDITOR_RULES_CONTENT, "utf-8");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackmem",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
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 (for Cursor and Windsurf) are skipped if they
120
- 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.
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
- 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);
905
1063
 
906
- At the start of every session, run:
907
- cm start
1064
+ if (memoriesError) fail(`Failed to load memories: ${memoriesError.message}`);
908
1065
 
909
- 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);
910
1072
 
911
- When you make a decision, discover a constraint,
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
- Types: decision, rejection, constraint, discovery
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, EDITOR_RULES_CONTENT, "utf-8");
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, EDITOR_RULES_CONTENT, "utf-8");
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