stackmem 1.0.9 → 1.0.11

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.
Files changed (3) hide show
  1. package/dist/cli.js +130 -17
  2. package/package.json +1 -1
  3. package/src/cli.ts +150 -14
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,6 +32,7 @@ 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>
37
38
  cm context [--project <name>] <task>
@@ -88,6 +89,13 @@ function cmdHelp() {
88
89
  cm delete [project] <memory-id>
89
90
  Delete a memory and any memory_links referencing it.
90
91
 
92
+ cm inspect [project] <memory-id>
93
+ Show a rich view of a single memory: type, content, created
94
+ date, access count, decay score, and resolved status, plus
95
+ every linked memory (in either link direction) with its link
96
+ score, and any execution_log fixes whose problem or solution
97
+ shares a key entity with the memory's content.
98
+
91
99
  cm compress [project]
92
100
  Cluster related unresolved memories (needs 20+) and collapse
93
101
  clusters of 3 or more into a single summary memory.
@@ -496,6 +504,86 @@ async function cmdDelete(project, memoryId) {
496
504
  fail(`Failed to delete memory: ${error.message}`);
497
505
  console.log(`Deleted memory [${memoryId}]`);
498
506
  }
507
+ // --- cm inspect ------------------------------------------------------------
508
+ function formatDate(dateString) {
509
+ return new Date(dateString).toLocaleDateString("en-US", {
510
+ month: "short",
511
+ day: "numeric",
512
+ year: "numeric",
513
+ });
514
+ }
515
+ async function cmdInspect(project, memoryId) {
516
+ const { data: memory, error } = await supabase
517
+ .from("memories")
518
+ .select("*")
519
+ .eq("project", project)
520
+ .eq("id", memoryId)
521
+ .single();
522
+ if (error || !memory)
523
+ fail(`Memory [${memoryId}] not found for "${project}".`);
524
+ const decayScore = computeDecayScore(new Date(memory.created_at), memory.access_count ?? 0);
525
+ const { data: links, error: linksError } = await supabase
526
+ .from("memory_links")
527
+ .select("source_id, target_id, score")
528
+ .or(`source_id.eq.${memoryId},target_id.eq.${memoryId}`);
529
+ if (linksError)
530
+ fail(`Failed to load memory links: ${linksError.message}`);
531
+ const linkedEntries = (links ?? []).map((link) => ({
532
+ linkedId: link.source_id === memoryId ? link.target_id : link.source_id,
533
+ score: link.score,
534
+ }));
535
+ let linkedMemories = [];
536
+ if (linkedEntries.length > 0) {
537
+ const linkedIds = linkedEntries.map((entry) => entry.linkedId);
538
+ const { data: linkedRows, error: linkedError } = await supabase
539
+ .from("memories")
540
+ .select("id, type, content, created_at")
541
+ .in("id", linkedIds);
542
+ if (linkedError)
543
+ fail(`Failed to load linked memories: ${linkedError.message}`);
544
+ const scoreById = new Map(linkedEntries.map((entry) => [entry.linkedId, entry.score]));
545
+ linkedMemories = (linkedRows ?? [])
546
+ .map((row) => ({ ...row, score: scoreById.get(row.id) ?? 0 }))
547
+ .sort((a, b) => b.score - a.score);
548
+ }
549
+ const entities = extractEntities(memory.content);
550
+ const { data: fixes, error: fixesError } = await supabase
551
+ .from("execution_log")
552
+ .select("problem, solution")
553
+ .eq("project", project);
554
+ if (fixesError)
555
+ fail(`Failed to load execution log: ${fixesError.message}`);
556
+ const relatedFixes = (fixes ?? []).filter((fix) => {
557
+ const fixEntities = extractEntities(`${fix.problem} ${fix.solution}`);
558
+ for (const entity of entities) {
559
+ if (fixEntities.has(entity))
560
+ return true;
561
+ }
562
+ return false;
563
+ });
564
+ const divider = "─".repeat(40);
565
+ console.log(divider);
566
+ console.log(`${memory.type.toUpperCase()} · accessed ${memory.access_count ?? 0}x · decay ${decayScore.toFixed(2)}`);
567
+ console.log(divider);
568
+ console.log(memory.content);
569
+ console.log("");
570
+ console.log(`Created: ${formatDate(memory.created_at)}`);
571
+ console.log(`Status: ${memory.resolved ? "resolved" : "unresolved"}`);
572
+ console.log("");
573
+ console.log(`LINKED MEMORIES (${linkedMemories.length})`);
574
+ for (const linked of linkedMemories) {
575
+ console.log(` └─ [score: ${linked.score.toFixed(2)}] ${linked.type.toUpperCase()}`);
576
+ console.log(` ${linked.content}`);
577
+ console.log(` ${formatDate(linked.created_at)}`);
578
+ console.log("");
579
+ }
580
+ if (relatedFixes.length > 0) {
581
+ console.log(`RELATED FIXES (${relatedFixes.length})`);
582
+ for (const fix of relatedFixes) {
583
+ console.log(` └─ FIX: ${fix.problem} → ${fix.solution}`);
584
+ }
585
+ }
586
+ }
499
587
  // --- cm init -------------------------------------------------------------
500
588
  // The hook logic lives in its own .mjs file (not directly in
501
589
  // .git/hooks/post-commit) so its module type is unambiguous — an
@@ -840,6 +928,14 @@ function writeProjectFile(project, resolvedPath) {
840
928
  // non-interactive). Returns how many memories were actually saved.
841
929
  async function seedMemories(project, resolvedPath) {
842
930
  let seededCount = 0;
931
+ const { data: existingRows, error: existingError } = await supabase
932
+ .from("memories")
933
+ .select("id, content")
934
+ .eq("project", project)
935
+ .eq("resolved", false);
936
+ if (existingError)
937
+ fail(`Failed to load memories: ${existingError.message}`);
938
+ const existingMemories = existingRows ?? [];
843
939
  const packageJsonPath = nodePath.join(resolvedPath, "package.json");
844
940
  if (nodeFs.existsSync(packageJsonPath)) {
845
941
  try {
@@ -849,14 +945,18 @@ async function seedMemories(project, resolvedPath) {
849
945
  ...Object.keys(pkg.devDependencies ?? {}),
850
946
  ];
851
947
  if (depNames.length > 0) {
852
- const { error } = await supabase.from("memories").insert({
853
- project,
854
- type: "discovery",
855
- content: `tech stack: ${depNames.join(", ")}`,
856
- device_id: getDeviceId(),
857
- });
858
- if (!error)
859
- seededCount++;
948
+ const content = `tech stack: ${depNames.join(", ")}`;
949
+ const decision = decideSaveAction(content, existingMemories);
950
+ if (decision.action !== "duplicate") {
951
+ const { error } = await supabase.from("memories").insert({
952
+ project,
953
+ type: "discovery",
954
+ content,
955
+ device_id: getDeviceId(),
956
+ });
957
+ if (!error)
958
+ seededCount++;
959
+ }
860
960
  }
861
961
  }
862
962
  catch {
@@ -871,14 +971,18 @@ async function seedMemories(project, resolvedPath) {
871
971
  .filter((line) => /^[A-Z_]+=/.test(line))
872
972
  .map((line) => line.split("=")[0]);
873
973
  if (varNames.length > 0) {
874
- const { error } = await supabase.from("memories").insert({
875
- project,
876
- type: "constraint",
877
- content: `required env vars: ${varNames.join(", ")}`,
878
- device_id: getDeviceId(),
879
- });
880
- if (!error)
881
- seededCount++;
974
+ const content = `required env vars: ${varNames.join(", ")}`;
975
+ const decision = decideSaveAction(content, existingMemories);
976
+ if (decision.action !== "duplicate") {
977
+ const { error } = await supabase.from("memories").insert({
978
+ project,
979
+ type: "constraint",
980
+ content,
981
+ device_id: getDeviceId(),
982
+ });
983
+ if (!error)
984
+ seededCount++;
985
+ }
882
986
  }
883
987
  }
884
988
  analyzeCodebase(resolvedPath);
@@ -1402,6 +1506,15 @@ async function main() {
1402
1506
  await cmdDelete(project, memoryId);
1403
1507
  break;
1404
1508
  }
1509
+ case "inspect": {
1510
+ const split = splitOptionalProject(args, 1);
1511
+ if (!split)
1512
+ usage();
1513
+ const project = requireProject(split.project);
1514
+ const [memoryId] = split.rest;
1515
+ await cmdInspect(project, memoryId);
1516
+ break;
1517
+ }
1405
1518
  case "compress": {
1406
1519
  const split = splitOptionalProject(args, 0);
1407
1520
  if (!split)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackmem",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
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,6 +51,7 @@ 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>
55
57
  cm context [--project <name>] <task>
@@ -107,6 +109,13 @@ function cmdHelp() {
107
109
  cm delete [project] <memory-id>
108
110
  Delete a memory and any memory_links referencing it.
109
111
 
112
+ cm inspect [project] <memory-id>
113
+ Show a rich view of a single memory: type, content, created
114
+ date, access count, decay score, and resolved status, plus
115
+ every linked memory (in either link direction) with its link
116
+ score, and any execution_log fixes whose problem or solution
117
+ shares a key entity with the memory's content.
118
+
110
119
  cm compress [project]
111
120
  Cluster related unresolved memories (needs 20+) and collapse
112
121
  clusters of 3 or more into a single summary memory.
@@ -593,6 +602,107 @@ async function cmdDelete(project: string, memoryId: string) {
593
602
  console.log(`Deleted memory [${memoryId}]`);
594
603
  }
595
604
 
605
+ // --- cm inspect ------------------------------------------------------------
606
+
607
+ function formatDate(dateString: string): string {
608
+ return new Date(dateString).toLocaleDateString("en-US", {
609
+ month: "short",
610
+ day: "numeric",
611
+ year: "numeric",
612
+ });
613
+ }
614
+
615
+ async function cmdInspect(project: string, memoryId: string) {
616
+ const { data: memory, error } = await supabase
617
+ .from("memories")
618
+ .select("*")
619
+ .eq("project", project)
620
+ .eq("id", memoryId)
621
+ .single();
622
+
623
+ if (error || !memory) fail(`Memory [${memoryId}] not found for "${project}".`);
624
+
625
+ const decayScore = computeDecayScore(new Date(memory.created_at), memory.access_count ?? 0);
626
+
627
+ const { data: links, error: linksError } = await supabase
628
+ .from("memory_links")
629
+ .select("source_id, target_id, score")
630
+ .or(`source_id.eq.${memoryId},target_id.eq.${memoryId}`);
631
+
632
+ if (linksError) fail(`Failed to load memory links: ${linksError.message}`);
633
+
634
+ const linkedEntries = (links ?? []).map((link) => ({
635
+ linkedId: link.source_id === memoryId ? link.target_id : link.source_id,
636
+ score: link.score,
637
+ }));
638
+
639
+ let linkedMemories: Array<{
640
+ id: string;
641
+ type: string;
642
+ content: string;
643
+ created_at: string;
644
+ score: number;
645
+ }> = [];
646
+
647
+ if (linkedEntries.length > 0) {
648
+ const linkedIds = linkedEntries.map((entry) => entry.linkedId);
649
+ const { data: linkedRows, error: linkedError } = await supabase
650
+ .from("memories")
651
+ .select("id, type, content, created_at")
652
+ .in("id", linkedIds);
653
+
654
+ if (linkedError) fail(`Failed to load linked memories: ${linkedError.message}`);
655
+
656
+ const scoreById = new Map(linkedEntries.map((entry) => [entry.linkedId, entry.score]));
657
+ linkedMemories = (linkedRows ?? [])
658
+ .map((row) => ({ ...row, score: scoreById.get(row.id) ?? 0 }))
659
+ .sort((a, b) => b.score - a.score);
660
+ }
661
+
662
+ const entities = extractEntities(memory.content);
663
+ const { data: fixes, error: fixesError } = await supabase
664
+ .from("execution_log")
665
+ .select("problem, solution")
666
+ .eq("project", project);
667
+
668
+ if (fixesError) fail(`Failed to load execution log: ${fixesError.message}`);
669
+
670
+ const relatedFixes = (fixes ?? []).filter((fix) => {
671
+ const fixEntities = extractEntities(`${fix.problem} ${fix.solution}`);
672
+ for (const entity of entities) {
673
+ if (fixEntities.has(entity)) return true;
674
+ }
675
+ return false;
676
+ });
677
+
678
+ const divider = "─".repeat(40);
679
+ console.log(divider);
680
+ console.log(
681
+ `${memory.type.toUpperCase()} · accessed ${memory.access_count ?? 0}x · decay ${decayScore.toFixed(2)}`
682
+ );
683
+ console.log(divider);
684
+ console.log(memory.content);
685
+ console.log("");
686
+ console.log(`Created: ${formatDate(memory.created_at)}`);
687
+ console.log(`Status: ${memory.resolved ? "resolved" : "unresolved"}`);
688
+ console.log("");
689
+
690
+ console.log(`LINKED MEMORIES (${linkedMemories.length})`);
691
+ for (const linked of linkedMemories) {
692
+ console.log(` └─ [score: ${linked.score.toFixed(2)}] ${linked.type.toUpperCase()}`);
693
+ console.log(` ${linked.content}`);
694
+ console.log(` ${formatDate(linked.created_at)}`);
695
+ console.log("");
696
+ }
697
+
698
+ if (relatedFixes.length > 0) {
699
+ console.log(`RELATED FIXES (${relatedFixes.length})`);
700
+ for (const fix of relatedFixes) {
701
+ console.log(` └─ FIX: ${fix.problem} → ${fix.solution}`);
702
+ }
703
+ }
704
+ }
705
+
596
706
  // --- cm init -------------------------------------------------------------
597
707
 
598
708
  // The hook logic lives in its own .mjs file (not directly in
@@ -957,6 +1067,16 @@ function writeProjectFile(project: string, resolvedPath: string): void {
957
1067
  async function seedMemories(project: string, resolvedPath: string): Promise<number> {
958
1068
  let seededCount = 0;
959
1069
 
1070
+ const { data: existingRows, error: existingError } = await supabase
1071
+ .from("memories")
1072
+ .select("id, content")
1073
+ .eq("project", project)
1074
+ .eq("resolved", false);
1075
+
1076
+ if (existingError) fail(`Failed to load memories: ${existingError.message}`);
1077
+
1078
+ const existingMemories = existingRows ?? [];
1079
+
960
1080
  const packageJsonPath = nodePath.join(resolvedPath, "package.json");
961
1081
  if (nodeFs.existsSync(packageJsonPath)) {
962
1082
  try {
@@ -966,13 +1086,17 @@ async function seedMemories(project: string, resolvedPath: string): Promise<numb
966
1086
  ...Object.keys(pkg.devDependencies ?? {}),
967
1087
  ];
968
1088
  if (depNames.length > 0) {
969
- const { error } = await supabase.from("memories").insert({
970
- project,
971
- type: "discovery",
972
- content: `tech stack: ${depNames.join(", ")}`,
973
- device_id: getDeviceId(),
974
- });
975
- if (!error) seededCount++;
1089
+ const content = `tech stack: ${depNames.join(", ")}`;
1090
+ const decision = decideSaveAction(content, existingMemories);
1091
+ if (decision.action !== "duplicate") {
1092
+ const { error } = await supabase.from("memories").insert({
1093
+ project,
1094
+ type: "discovery",
1095
+ content,
1096
+ device_id: getDeviceId(),
1097
+ });
1098
+ if (!error) seededCount++;
1099
+ }
976
1100
  }
977
1101
  } catch {
978
1102
  // Malformed package.json — skip the tech-stack memory.
@@ -988,13 +1112,17 @@ async function seedMemories(project: string, resolvedPath: string): Promise<numb
988
1112
  .map((line) => line.split("=")[0]);
989
1113
 
990
1114
  if (varNames.length > 0) {
991
- const { error } = await supabase.from("memories").insert({
992
- project,
993
- type: "constraint",
994
- content: `required env vars: ${varNames.join(", ")}`,
995
- device_id: getDeviceId(),
996
- });
997
- if (!error) seededCount++;
1115
+ const content = `required env vars: ${varNames.join(", ")}`;
1116
+ const decision = decideSaveAction(content, existingMemories);
1117
+ if (decision.action !== "duplicate") {
1118
+ const { error } = await supabase.from("memories").insert({
1119
+ project,
1120
+ type: "constraint",
1121
+ content,
1122
+ device_id: getDeviceId(),
1123
+ });
1124
+ if (!error) seededCount++;
1125
+ }
998
1126
  }
999
1127
  }
1000
1128
 
@@ -1582,6 +1710,14 @@ async function main() {
1582
1710
  await cmdDelete(project, memoryId);
1583
1711
  break;
1584
1712
  }
1713
+ case "inspect": {
1714
+ const split = splitOptionalProject(args, 1);
1715
+ if (!split) usage();
1716
+ const project = requireProject(split.project);
1717
+ const [memoryId] = split.rest;
1718
+ await cmdInspect(project, memoryId);
1719
+ break;
1720
+ }
1585
1721
  case "compress": {
1586
1722
  const split = splitOptionalProject(args, 0);
1587
1723
  if (!split) usage();