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.
- package/dist/cli.js +130 -17
- package/package.json +1 -1
- 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
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
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
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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();
|