stackmem 1.0.3 → 1.0.4
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/README.md +7 -1
- package/dist/cli.js +205 -46
- package/package.json +1 -1
- package/src/cli.ts +225 -46
package/README.md
CHANGED
|
@@ -16,7 +16,13 @@ Then in any git repo:
|
|
|
16
16
|
cm init <project> .
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
`cm init <project> .` does three things automatically:
|
|
20
|
+
|
|
21
|
+
1. Installs a git post-commit hook
|
|
22
|
+
2. Registers the stackmem MCP server with Claude Code
|
|
23
|
+
3. Writes a CLAUDE.md to the project
|
|
24
|
+
|
|
25
|
+
After that, memories load automatically when you open Claude Code and save automatically on every commit. No further setup needed.
|
|
20
26
|
|
|
21
27
|
## Usage
|
|
22
28
|
|
package/dist/cli.js
CHANGED
|
@@ -25,63 +25,68 @@ const TYPE_LABELS = {
|
|
|
25
25
|
};
|
|
26
26
|
function usage() {
|
|
27
27
|
console.error(`Usage:
|
|
28
|
-
cm start
|
|
29
|
-
cm save
|
|
30
|
-
cm fix
|
|
31
|
-
cm analyze
|
|
32
|
-
cm search
|
|
33
|
-
cm resolve
|
|
34
|
-
cm delete
|
|
35
|
-
cm compress
|
|
28
|
+
cm start [project]
|
|
29
|
+
cm save [project] <type> <content> [--force] (type: decision|rejection|constraint|discovery)
|
|
30
|
+
cm fix [project] <problem> <solution>
|
|
31
|
+
cm analyze [project] <path>
|
|
32
|
+
cm search [project] <query>
|
|
33
|
+
cm resolve [project] <memory-id>
|
|
34
|
+
cm delete [project] <memory-id>
|
|
35
|
+
cm compress [project]
|
|
36
36
|
cm init <project> <path>
|
|
37
|
-
cm context
|
|
38
|
-
cm help
|
|
37
|
+
cm context [project] <task>
|
|
38
|
+
cm help
|
|
39
|
+
|
|
40
|
+
[project] is optional if a .stackmem file exists in the current
|
|
41
|
+
directory (written by 'cm init'). Otherwise it must be given explicitly.`);
|
|
39
42
|
process.exit(1);
|
|
40
43
|
}
|
|
41
44
|
function cmdHelp() {
|
|
42
45
|
console.log(`cm — coding memory CLI
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
[project] is optional everywhere below if a .stackmem file exists in
|
|
48
|
+
the current directory (written by 'cm init'). Otherwise pass it explicitly.
|
|
49
|
+
|
|
50
|
+
cm start [project]
|
|
45
51
|
Load all unresolved memories for a project, sorted by decay
|
|
46
52
|
score and capped at a 2000 token context budget, grouped by
|
|
47
53
|
type (decisions, constraints, discoveries, rejections), with
|
|
48
54
|
linked memory ids shown under each entry.
|
|
49
55
|
|
|
50
|
-
cm save
|
|
56
|
+
cm save [project] <type> <content> [--force]
|
|
51
57
|
Save a new memory. <type> is one of: decision, rejection,
|
|
52
58
|
constraint, discovery. Automatically links to related existing
|
|
53
59
|
memories. If the new memory contradicts an existing one, prompts
|
|
54
60
|
to keep the old memory or replace it — pass --force to skip the
|
|
55
61
|
prompt and always replace.
|
|
56
62
|
|
|
57
|
-
cm fix
|
|
63
|
+
cm fix [project] <problem> <solution>
|
|
58
64
|
Record a problem and its solution for future reference.
|
|
59
65
|
|
|
60
|
-
cm analyze
|
|
66
|
+
cm analyze [project] <path>
|
|
61
67
|
Build an AST index (files, functions, classes) for the codebase
|
|
62
68
|
at <path>.
|
|
63
69
|
|
|
64
|
-
cm search
|
|
70
|
+
cm search [project] <query>
|
|
65
71
|
Search saved memories and past fixes for a project matching
|
|
66
72
|
<query>.
|
|
67
73
|
|
|
68
|
-
cm resolve
|
|
74
|
+
cm resolve [project] <memory-id>
|
|
69
75
|
Mark a memory as resolved.
|
|
70
76
|
|
|
71
|
-
cm delete
|
|
77
|
+
cm delete [project] <memory-id>
|
|
72
78
|
Delete a memory and any memory_links referencing it.
|
|
73
79
|
|
|
74
|
-
cm compress
|
|
80
|
+
cm compress [project]
|
|
75
81
|
Cluster related unresolved memories (needs 20+) and collapse
|
|
76
82
|
clusters of 3 or more into a single summary memory.
|
|
77
83
|
|
|
78
84
|
cm init <project> <path>
|
|
79
|
-
Install a git post-commit hook in the repo at <path
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
commit.
|
|
85
|
+
Install a git post-commit hook in the repo at <path>, register
|
|
86
|
+
the stackmem MCP server with Claude Code, write a CLAUDE.md and
|
|
87
|
+
.stackmem file, and seed initial memories from the project scan.
|
|
83
88
|
|
|
84
|
-
cm context
|
|
89
|
+
cm context [project] <task>
|
|
85
90
|
Return only the top 5 memories most relevant to a specific
|
|
86
91
|
task, ranked by a blend of task relevance and decay score,
|
|
87
92
|
as a markdown block capped at a 2000 token budget.
|
|
@@ -93,6 +98,45 @@ function fail(message) {
|
|
|
93
98
|
console.error(`Error: ${message}`);
|
|
94
99
|
process.exit(1);
|
|
95
100
|
}
|
|
101
|
+
// --- optional project resolution ------------------------------------------
|
|
102
|
+
const STACKMEM_FILE = ".stackmem";
|
|
103
|
+
function readProjectFromFile() {
|
|
104
|
+
const stackmemPath = nodePath.join(process.cwd(), STACKMEM_FILE);
|
|
105
|
+
if (!nodeFs.existsSync(stackmemPath))
|
|
106
|
+
return null;
|
|
107
|
+
const content = nodeFs.readFileSync(stackmemPath, "utf-8").trim();
|
|
108
|
+
return content.length > 0 ? content : null;
|
|
109
|
+
}
|
|
110
|
+
function requireProject(explicit) {
|
|
111
|
+
if (explicit)
|
|
112
|
+
return explicit;
|
|
113
|
+
const fromFile = readProjectFromFile();
|
|
114
|
+
if (fromFile)
|
|
115
|
+
return fromFile;
|
|
116
|
+
console.error("No project specified and no .stackmem file found. Run 'cm init <project> .' first or pass a project name.");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
// For commands whose non-project args have a fixed count: if exactly that
|
|
120
|
+
// many args are given, project was omitted; if one more, the first is the
|
|
121
|
+
// project. Anything else is a usage error.
|
|
122
|
+
function splitOptionalProject(args, fixedArgCount) {
|
|
123
|
+
if (args.length === fixedArgCount)
|
|
124
|
+
return { project: null, rest: args };
|
|
125
|
+
if (args.length === fixedArgCount + 1)
|
|
126
|
+
return { project: args[0], rest: args.slice(1) };
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
// For commands whose remaining arg is free-form text (a query or task),
|
|
130
|
+
// arg count can't disambiguate an omitted project from a multi-word query —
|
|
131
|
+
// "cm search bug fix" is ambiguous by count alone. Use .stackmem's presence
|
|
132
|
+
// instead: if it exists, nothing is positionally a project.
|
|
133
|
+
function splitProjectFromVariadic(args) {
|
|
134
|
+
if (readProjectFromFile() !== null) {
|
|
135
|
+
return { project: null, rest: args };
|
|
136
|
+
}
|
|
137
|
+
const [project = null, ...rest] = args;
|
|
138
|
+
return { project, rest };
|
|
139
|
+
}
|
|
96
140
|
// --- cm start ----------------------------------------------------------
|
|
97
141
|
async function cmdStart(project) {
|
|
98
142
|
const { data, error } = await supabase
|
|
@@ -389,11 +433,20 @@ async function cmdDelete(project, memoryId) {
|
|
|
389
433
|
// .git/hooks/post-commit) so its module type is unambiguous — an
|
|
390
434
|
// extensionless file run by git would otherwise have its CommonJS/ESM
|
|
391
435
|
// interpretation depend on the target repo's own package.json.
|
|
392
|
-
function buildHookLogicScript(
|
|
436
|
+
function buildHookLogicScript(cliPath) {
|
|
393
437
|
return `#!/usr/bin/env node
|
|
394
438
|
import { execFileSync } from "node:child_process";
|
|
439
|
+
import { readFileSync } from "node:fs";
|
|
440
|
+
import { join } from "node:path";
|
|
441
|
+
|
|
442
|
+
const stackmemPath = join(process.cwd(), ".stackmem");
|
|
443
|
+
let PROJECT;
|
|
444
|
+
try {
|
|
445
|
+
PROJECT = readFileSync(stackmemPath, "utf8").trim();
|
|
446
|
+
} catch {
|
|
447
|
+
process.exit(0); // no .stackmem, skip silently
|
|
448
|
+
}
|
|
395
449
|
|
|
396
|
-
const PROJECT = ${JSON.stringify(project)};
|
|
397
450
|
const CLI_PATH = ${JSON.stringify(cliPath)};
|
|
398
451
|
|
|
399
452
|
function git(args) {
|
|
@@ -611,6 +664,69 @@ cm save ${project} <type> "<content>"
|
|
|
611
664
|
nodeFs.writeFileSync(claudeMdPath, content, "utf-8");
|
|
612
665
|
console.log("CLAUDE.md written.");
|
|
613
666
|
}
|
|
667
|
+
function writeProjectFile(project, resolvedPath) {
|
|
668
|
+
const stackmemPath = nodePath.join(resolvedPath, STACKMEM_FILE);
|
|
669
|
+
nodeFs.writeFileSync(stackmemPath, `${project}\n`, "utf-8");
|
|
670
|
+
}
|
|
671
|
+
// Scans the freshly-initialized project for a few cheap, high-signal facts
|
|
672
|
+
// and saves them as memories directly (bypassing cmdSave's duplicate/conflict
|
|
673
|
+
// prompt — there's nothing to conflict with yet, and init must stay
|
|
674
|
+
// non-interactive). Returns how many memories were actually saved.
|
|
675
|
+
async function seedMemories(project, resolvedPath) {
|
|
676
|
+
let seededCount = 0;
|
|
677
|
+
const packageJsonPath = nodePath.join(resolvedPath, "package.json");
|
|
678
|
+
if (nodeFs.existsSync(packageJsonPath)) {
|
|
679
|
+
try {
|
|
680
|
+
const pkg = JSON.parse(nodeFs.readFileSync(packageJsonPath, "utf-8"));
|
|
681
|
+
const depNames = [
|
|
682
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
683
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
684
|
+
];
|
|
685
|
+
if (depNames.length > 0) {
|
|
686
|
+
const { error } = await supabase.from("memories").insert({
|
|
687
|
+
project,
|
|
688
|
+
type: "discovery",
|
|
689
|
+
content: `tech stack: ${depNames.join(", ")}`,
|
|
690
|
+
device_id: getDeviceId(),
|
|
691
|
+
});
|
|
692
|
+
if (!error)
|
|
693
|
+
seededCount++;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
// Malformed package.json — skip the tech-stack memory.
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const envExamplePath = nodePath.join(resolvedPath, ".env.example");
|
|
701
|
+
if (nodeFs.existsSync(envExamplePath)) {
|
|
702
|
+
const varNames = nodeFs
|
|
703
|
+
.readFileSync(envExamplePath, "utf-8")
|
|
704
|
+
.split("\n")
|
|
705
|
+
.filter((line) => /^[A-Z_]+=/.test(line))
|
|
706
|
+
.map((line) => line.split("=")[0]);
|
|
707
|
+
if (varNames.length > 0) {
|
|
708
|
+
const { error } = await supabase.from("memories").insert({
|
|
709
|
+
project,
|
|
710
|
+
type: "constraint",
|
|
711
|
+
content: `required env vars: ${varNames.join(", ")}`,
|
|
712
|
+
device_id: getDeviceId(),
|
|
713
|
+
});
|
|
714
|
+
if (!error)
|
|
715
|
+
seededCount++;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
analyzeCodebase(resolvedPath);
|
|
719
|
+
return seededCount;
|
|
720
|
+
}
|
|
721
|
+
// Smoke-tests the same anon-key + device-header path session_start/cmdStart
|
|
722
|
+
// use, without pulling in all of cmdStart's scoring/formatting logic.
|
|
723
|
+
async function verifyBackendConnection(project) {
|
|
724
|
+
const { error } = await supabase
|
|
725
|
+
.from("memories")
|
|
726
|
+
.select("id", { count: "exact", head: true })
|
|
727
|
+
.eq("project", project);
|
|
728
|
+
return !error;
|
|
729
|
+
}
|
|
614
730
|
async function cmdInit(project, targetPath) {
|
|
615
731
|
const resolvedPath = nodePath.resolve(targetPath);
|
|
616
732
|
const gitDir = nodePath.join(resolvedPath, ".git");
|
|
@@ -621,7 +737,7 @@ async function cmdInit(project, targetPath) {
|
|
|
621
737
|
const codingMemoryDir = nodePath.join(resolvedPath, ".coding-memory");
|
|
622
738
|
nodeFs.mkdirSync(codingMemoryDir, { recursive: true });
|
|
623
739
|
const hookLogicPath = nodePath.join(codingMemoryDir, "post-commit-hook.mjs");
|
|
624
|
-
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(
|
|
740
|
+
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(DIST_CLI_PATH), "utf-8");
|
|
625
741
|
const hooksDir = nodePath.join(gitDir, "hooks");
|
|
626
742
|
nodeFs.mkdirSync(hooksDir, { recursive: true });
|
|
627
743
|
const hookPath = nodePath.join(hooksDir, "post-commit");
|
|
@@ -630,6 +746,26 @@ async function cmdInit(project, targetPath) {
|
|
|
630
746
|
console.log(`coding-memory hook installed for project ${project} at ${targetPath}`);
|
|
631
747
|
registerMcpServer(DIST_INDEX_PATH);
|
|
632
748
|
writeClaudeMd(project, resolvedPath);
|
|
749
|
+
const seededCount = await seedMemories(project, resolvedPath);
|
|
750
|
+
console.log(`Seeded ${seededCount} initial memories from project scan.`);
|
|
751
|
+
const backendOk = await verifyBackendConnection(project);
|
|
752
|
+
console.log("");
|
|
753
|
+
console.log("✓ Git hook installed");
|
|
754
|
+
console.log("✓ MCP server registered");
|
|
755
|
+
console.log("✓ CLAUDE.md written");
|
|
756
|
+
console.log(`✓ Seeded ${seededCount} memories from project scan`);
|
|
757
|
+
if (backendOk) {
|
|
758
|
+
console.log("✓ Backend connection verified");
|
|
759
|
+
console.log("");
|
|
760
|
+
console.log(`stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
console.log("✗ Backend connection failed — run 'cm doctor' (once we build it)");
|
|
764
|
+
}
|
|
765
|
+
// Written last and deliberately: if a commit (and hence the post-commit
|
|
766
|
+
// hook) fires anywhere during init, it must still see whatever project
|
|
767
|
+
// .stackmem pointed to before this run, not the one being initialized now.
|
|
768
|
+
writeProjectFile(project, resolvedPath);
|
|
633
769
|
}
|
|
634
770
|
// --- cm context ------------------------------------------------------------
|
|
635
771
|
const CONTEXT_TYPE_ORDER = ["decision", "constraint", "rejection", "discovery"];
|
|
@@ -679,9 +815,10 @@ async function main() {
|
|
|
679
815
|
const [, , command, ...args] = process.argv;
|
|
680
816
|
switch (command) {
|
|
681
817
|
case "start": {
|
|
682
|
-
const
|
|
683
|
-
if (!
|
|
818
|
+
const split = splitOptionalProject(args, 0);
|
|
819
|
+
if (!split)
|
|
684
820
|
usage();
|
|
821
|
+
const project = requireProject(split.project);
|
|
685
822
|
await cmdStart(project);
|
|
686
823
|
break;
|
|
687
824
|
}
|
|
@@ -691,51 +828,72 @@ async function main() {
|
|
|
691
828
|
const positional = force
|
|
692
829
|
? [...args.slice(0, forceIndex), ...args.slice(forceIndex + 1)]
|
|
693
830
|
: args;
|
|
694
|
-
|
|
695
|
-
|
|
831
|
+
let explicitProject;
|
|
832
|
+
let type;
|
|
833
|
+
let rest;
|
|
834
|
+
if (MEMORY_TYPES.includes(positional[0])) {
|
|
835
|
+
explicitProject = null;
|
|
836
|
+
[type, ...rest] = positional;
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
explicitProject = positional[0] ?? null;
|
|
840
|
+
[, type, ...rest] = positional;
|
|
841
|
+
}
|
|
842
|
+
if (!type || rest.length === 0)
|
|
696
843
|
usage();
|
|
844
|
+
const project = requireProject(explicitProject);
|
|
697
845
|
await cmdSave(project, type, rest.join(" "), force);
|
|
698
846
|
break;
|
|
699
847
|
}
|
|
700
848
|
case "fix": {
|
|
701
|
-
const
|
|
702
|
-
if (!
|
|
849
|
+
const split = splitOptionalProject(args, 2);
|
|
850
|
+
if (!split)
|
|
703
851
|
usage();
|
|
852
|
+
const project = requireProject(split.project);
|
|
853
|
+
const [problem, solution] = split.rest;
|
|
704
854
|
await cmdFix(project, problem, solution);
|
|
705
855
|
break;
|
|
706
856
|
}
|
|
707
857
|
case "analyze": {
|
|
708
|
-
const
|
|
709
|
-
if (!
|
|
858
|
+
const split = splitOptionalProject(args, 1);
|
|
859
|
+
if (!split)
|
|
710
860
|
usage();
|
|
861
|
+
const project = requireProject(split.project);
|
|
862
|
+
const [path] = split.rest;
|
|
711
863
|
await cmdAnalyze(project, path);
|
|
712
864
|
break;
|
|
713
865
|
}
|
|
714
866
|
case "search": {
|
|
715
|
-
const
|
|
716
|
-
|
|
867
|
+
const split = splitProjectFromVariadic(args);
|
|
868
|
+
const project = requireProject(split.project);
|
|
869
|
+
if (split.rest.length === 0)
|
|
717
870
|
usage();
|
|
718
|
-
await cmdSearch(project, rest.join(" "));
|
|
871
|
+
await cmdSearch(project, split.rest.join(" "));
|
|
719
872
|
break;
|
|
720
873
|
}
|
|
721
874
|
case "resolve": {
|
|
722
|
-
const
|
|
723
|
-
if (!
|
|
875
|
+
const split = splitOptionalProject(args, 1);
|
|
876
|
+
if (!split)
|
|
724
877
|
usage();
|
|
878
|
+
const project = requireProject(split.project);
|
|
879
|
+
const [memoryId] = split.rest;
|
|
725
880
|
await cmdResolve(project, memoryId);
|
|
726
881
|
break;
|
|
727
882
|
}
|
|
728
883
|
case "delete": {
|
|
729
|
-
const
|
|
730
|
-
if (!
|
|
884
|
+
const split = splitOptionalProject(args, 1);
|
|
885
|
+
if (!split)
|
|
731
886
|
usage();
|
|
887
|
+
const project = requireProject(split.project);
|
|
888
|
+
const [memoryId] = split.rest;
|
|
732
889
|
await cmdDelete(project, memoryId);
|
|
733
890
|
break;
|
|
734
891
|
}
|
|
735
892
|
case "compress": {
|
|
736
|
-
const
|
|
737
|
-
if (!
|
|
893
|
+
const split = splitOptionalProject(args, 0);
|
|
894
|
+
if (!split)
|
|
738
895
|
usage();
|
|
896
|
+
const project = requireProject(split.project);
|
|
739
897
|
await cmdCompress(project);
|
|
740
898
|
break;
|
|
741
899
|
}
|
|
@@ -747,10 +905,11 @@ async function main() {
|
|
|
747
905
|
break;
|
|
748
906
|
}
|
|
749
907
|
case "context": {
|
|
750
|
-
const
|
|
751
|
-
|
|
908
|
+
const split = splitProjectFromVariadic(args);
|
|
909
|
+
const project = requireProject(split.project);
|
|
910
|
+
if (split.rest.length === 0)
|
|
752
911
|
usage();
|
|
753
|
-
await cmdContext(project, rest.join(" "));
|
|
912
|
+
await cmdContext(project, split.rest.join(" "));
|
|
754
913
|
break;
|
|
755
914
|
}
|
|
756
915
|
case "help": {
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -39,64 +39,69 @@ const TYPE_LABELS: Record<MemoryType, string> = {
|
|
|
39
39
|
|
|
40
40
|
function usage(): never {
|
|
41
41
|
console.error(`Usage:
|
|
42
|
-
cm start
|
|
43
|
-
cm save
|
|
44
|
-
cm fix
|
|
45
|
-
cm analyze
|
|
46
|
-
cm search
|
|
47
|
-
cm resolve
|
|
48
|
-
cm delete
|
|
49
|
-
cm compress
|
|
42
|
+
cm start [project]
|
|
43
|
+
cm save [project] <type> <content> [--force] (type: decision|rejection|constraint|discovery)
|
|
44
|
+
cm fix [project] <problem> <solution>
|
|
45
|
+
cm analyze [project] <path>
|
|
46
|
+
cm search [project] <query>
|
|
47
|
+
cm resolve [project] <memory-id>
|
|
48
|
+
cm delete [project] <memory-id>
|
|
49
|
+
cm compress [project]
|
|
50
50
|
cm init <project> <path>
|
|
51
|
-
cm context
|
|
52
|
-
cm help
|
|
51
|
+
cm context [project] <task>
|
|
52
|
+
cm help
|
|
53
|
+
|
|
54
|
+
[project] is optional if a .stackmem file exists in the current
|
|
55
|
+
directory (written by 'cm init'). Otherwise it must be given explicitly.`);
|
|
53
56
|
process.exit(1);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
function cmdHelp() {
|
|
57
60
|
console.log(`cm — coding memory CLI
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
[project] is optional everywhere below if a .stackmem file exists in
|
|
63
|
+
the current directory (written by 'cm init'). Otherwise pass it explicitly.
|
|
64
|
+
|
|
65
|
+
cm start [project]
|
|
60
66
|
Load all unresolved memories for a project, sorted by decay
|
|
61
67
|
score and capped at a 2000 token context budget, grouped by
|
|
62
68
|
type (decisions, constraints, discoveries, rejections), with
|
|
63
69
|
linked memory ids shown under each entry.
|
|
64
70
|
|
|
65
|
-
cm save
|
|
71
|
+
cm save [project] <type> <content> [--force]
|
|
66
72
|
Save a new memory. <type> is one of: decision, rejection,
|
|
67
73
|
constraint, discovery. Automatically links to related existing
|
|
68
74
|
memories. If the new memory contradicts an existing one, prompts
|
|
69
75
|
to keep the old memory or replace it — pass --force to skip the
|
|
70
76
|
prompt and always replace.
|
|
71
77
|
|
|
72
|
-
cm fix
|
|
78
|
+
cm fix [project] <problem> <solution>
|
|
73
79
|
Record a problem and its solution for future reference.
|
|
74
80
|
|
|
75
|
-
cm analyze
|
|
81
|
+
cm analyze [project] <path>
|
|
76
82
|
Build an AST index (files, functions, classes) for the codebase
|
|
77
83
|
at <path>.
|
|
78
84
|
|
|
79
|
-
cm search
|
|
85
|
+
cm search [project] <query>
|
|
80
86
|
Search saved memories and past fixes for a project matching
|
|
81
87
|
<query>.
|
|
82
88
|
|
|
83
|
-
cm resolve
|
|
89
|
+
cm resolve [project] <memory-id>
|
|
84
90
|
Mark a memory as resolved.
|
|
85
91
|
|
|
86
|
-
cm delete
|
|
92
|
+
cm delete [project] <memory-id>
|
|
87
93
|
Delete a memory and any memory_links referencing it.
|
|
88
94
|
|
|
89
|
-
cm compress
|
|
95
|
+
cm compress [project]
|
|
90
96
|
Cluster related unresolved memories (needs 20+) and collapse
|
|
91
97
|
clusters of 3 or more into a single summary memory.
|
|
92
98
|
|
|
93
99
|
cm init <project> <path>
|
|
94
|
-
Install a git post-commit hook in the repo at <path
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
commit.
|
|
100
|
+
Install a git post-commit hook in the repo at <path>, register
|
|
101
|
+
the stackmem MCP server with Claude Code, write a CLAUDE.md and
|
|
102
|
+
.stackmem file, and seed initial memories from the project scan.
|
|
98
103
|
|
|
99
|
-
cm context
|
|
104
|
+
cm context [project] <task>
|
|
100
105
|
Return only the top 5 memories most relevant to a specific
|
|
101
106
|
task, ranked by a blend of task relevance and decay score,
|
|
102
107
|
as a markdown block capped at a 2000 token budget.
|
|
@@ -110,6 +115,53 @@ function fail(message: string): never {
|
|
|
110
115
|
process.exit(1);
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
// --- optional project resolution ------------------------------------------
|
|
119
|
+
|
|
120
|
+
const STACKMEM_FILE = ".stackmem";
|
|
121
|
+
|
|
122
|
+
function readProjectFromFile(): string | null {
|
|
123
|
+
const stackmemPath = nodePath.join(process.cwd(), STACKMEM_FILE);
|
|
124
|
+
if (!nodeFs.existsSync(stackmemPath)) return null;
|
|
125
|
+
const content = nodeFs.readFileSync(stackmemPath, "utf-8").trim();
|
|
126
|
+
return content.length > 0 ? content : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function requireProject(explicit: string | null): string {
|
|
130
|
+
if (explicit) return explicit;
|
|
131
|
+
|
|
132
|
+
const fromFile = readProjectFromFile();
|
|
133
|
+
if (fromFile) return fromFile;
|
|
134
|
+
|
|
135
|
+
console.error(
|
|
136
|
+
"No project specified and no .stackmem file found. Run 'cm init <project> .' first or pass a project name."
|
|
137
|
+
);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// For commands whose non-project args have a fixed count: if exactly that
|
|
142
|
+
// many args are given, project was omitted; if one more, the first is the
|
|
143
|
+
// project. Anything else is a usage error.
|
|
144
|
+
function splitOptionalProject(
|
|
145
|
+
args: string[],
|
|
146
|
+
fixedArgCount: number
|
|
147
|
+
): { project: string | null; rest: string[] } | null {
|
|
148
|
+
if (args.length === fixedArgCount) return { project: null, rest: args };
|
|
149
|
+
if (args.length === fixedArgCount + 1) return { project: args[0], rest: args.slice(1) };
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// For commands whose remaining arg is free-form text (a query or task),
|
|
154
|
+
// arg count can't disambiguate an omitted project from a multi-word query —
|
|
155
|
+
// "cm search bug fix" is ambiguous by count alone. Use .stackmem's presence
|
|
156
|
+
// instead: if it exists, nothing is positionally a project.
|
|
157
|
+
function splitProjectFromVariadic(args: string[]): { project: string | null; rest: string[] } {
|
|
158
|
+
if (readProjectFromFile() !== null) {
|
|
159
|
+
return { project: null, rest: args };
|
|
160
|
+
}
|
|
161
|
+
const [project = null, ...rest] = args;
|
|
162
|
+
return { project, rest };
|
|
163
|
+
}
|
|
164
|
+
|
|
113
165
|
// --- cm start ----------------------------------------------------------
|
|
114
166
|
|
|
115
167
|
async function cmdStart(project: string) {
|
|
@@ -471,11 +523,20 @@ async function cmdDelete(project: string, memoryId: string) {
|
|
|
471
523
|
// .git/hooks/post-commit) so its module type is unambiguous — an
|
|
472
524
|
// extensionless file run by git would otherwise have its CommonJS/ESM
|
|
473
525
|
// interpretation depend on the target repo's own package.json.
|
|
474
|
-
function buildHookLogicScript(
|
|
526
|
+
function buildHookLogicScript(cliPath: string): string {
|
|
475
527
|
return `#!/usr/bin/env node
|
|
476
528
|
import { execFileSync } from "node:child_process";
|
|
529
|
+
import { readFileSync } from "node:fs";
|
|
530
|
+
import { join } from "node:path";
|
|
531
|
+
|
|
532
|
+
const stackmemPath = join(process.cwd(), ".stackmem");
|
|
533
|
+
let PROJECT;
|
|
534
|
+
try {
|
|
535
|
+
PROJECT = readFileSync(stackmemPath, "utf8").trim();
|
|
536
|
+
} catch {
|
|
537
|
+
process.exit(0); // no .stackmem, skip silently
|
|
538
|
+
}
|
|
477
539
|
|
|
478
|
-
const PROJECT = ${JSON.stringify(project)};
|
|
479
540
|
const CLI_PATH = ${JSON.stringify(cliPath)};
|
|
480
541
|
|
|
481
542
|
function git(args) {
|
|
@@ -703,6 +764,75 @@ cm save ${project} <type> "<content>"
|
|
|
703
764
|
console.log("CLAUDE.md written.");
|
|
704
765
|
}
|
|
705
766
|
|
|
767
|
+
function writeProjectFile(project: string, resolvedPath: string): void {
|
|
768
|
+
const stackmemPath = nodePath.join(resolvedPath, STACKMEM_FILE);
|
|
769
|
+
nodeFs.writeFileSync(stackmemPath, `${project}\n`, "utf-8");
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Scans the freshly-initialized project for a few cheap, high-signal facts
|
|
773
|
+
// and saves them as memories directly (bypassing cmdSave's duplicate/conflict
|
|
774
|
+
// prompt — there's nothing to conflict with yet, and init must stay
|
|
775
|
+
// non-interactive). Returns how many memories were actually saved.
|
|
776
|
+
async function seedMemories(project: string, resolvedPath: string): Promise<number> {
|
|
777
|
+
let seededCount = 0;
|
|
778
|
+
|
|
779
|
+
const packageJsonPath = nodePath.join(resolvedPath, "package.json");
|
|
780
|
+
if (nodeFs.existsSync(packageJsonPath)) {
|
|
781
|
+
try {
|
|
782
|
+
const pkg = JSON.parse(nodeFs.readFileSync(packageJsonPath, "utf-8"));
|
|
783
|
+
const depNames = [
|
|
784
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
785
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
786
|
+
];
|
|
787
|
+
if (depNames.length > 0) {
|
|
788
|
+
const { error } = await supabase.from("memories").insert({
|
|
789
|
+
project,
|
|
790
|
+
type: "discovery",
|
|
791
|
+
content: `tech stack: ${depNames.join(", ")}`,
|
|
792
|
+
device_id: getDeviceId(),
|
|
793
|
+
});
|
|
794
|
+
if (!error) seededCount++;
|
|
795
|
+
}
|
|
796
|
+
} catch {
|
|
797
|
+
// Malformed package.json — skip the tech-stack memory.
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const envExamplePath = nodePath.join(resolvedPath, ".env.example");
|
|
802
|
+
if (nodeFs.existsSync(envExamplePath)) {
|
|
803
|
+
const varNames = nodeFs
|
|
804
|
+
.readFileSync(envExamplePath, "utf-8")
|
|
805
|
+
.split("\n")
|
|
806
|
+
.filter((line) => /^[A-Z_]+=/.test(line))
|
|
807
|
+
.map((line) => line.split("=")[0]);
|
|
808
|
+
|
|
809
|
+
if (varNames.length > 0) {
|
|
810
|
+
const { error } = await supabase.from("memories").insert({
|
|
811
|
+
project,
|
|
812
|
+
type: "constraint",
|
|
813
|
+
content: `required env vars: ${varNames.join(", ")}`,
|
|
814
|
+
device_id: getDeviceId(),
|
|
815
|
+
});
|
|
816
|
+
if (!error) seededCount++;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
analyzeCodebase(resolvedPath);
|
|
821
|
+
|
|
822
|
+
return seededCount;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Smoke-tests the same anon-key + device-header path session_start/cmdStart
|
|
826
|
+
// use, without pulling in all of cmdStart's scoring/formatting logic.
|
|
827
|
+
async function verifyBackendConnection(project: string): Promise<boolean> {
|
|
828
|
+
const { error } = await supabase
|
|
829
|
+
.from("memories")
|
|
830
|
+
.select("id", { count: "exact", head: true })
|
|
831
|
+
.eq("project", project);
|
|
832
|
+
|
|
833
|
+
return !error;
|
|
834
|
+
}
|
|
835
|
+
|
|
706
836
|
async function cmdInit(project: string, targetPath: string) {
|
|
707
837
|
const resolvedPath = nodePath.resolve(targetPath);
|
|
708
838
|
const gitDir = nodePath.join(resolvedPath, ".git");
|
|
@@ -716,7 +846,7 @@ async function cmdInit(project: string, targetPath: string) {
|
|
|
716
846
|
nodeFs.mkdirSync(codingMemoryDir, { recursive: true });
|
|
717
847
|
|
|
718
848
|
const hookLogicPath = nodePath.join(codingMemoryDir, "post-commit-hook.mjs");
|
|
719
|
-
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(
|
|
849
|
+
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(DIST_CLI_PATH), "utf-8");
|
|
720
850
|
|
|
721
851
|
const hooksDir = nodePath.join(gitDir, "hooks");
|
|
722
852
|
nodeFs.mkdirSync(hooksDir, { recursive: true });
|
|
@@ -729,6 +859,31 @@ async function cmdInit(project: string, targetPath: string) {
|
|
|
729
859
|
|
|
730
860
|
registerMcpServer(DIST_INDEX_PATH);
|
|
731
861
|
writeClaudeMd(project, resolvedPath);
|
|
862
|
+
|
|
863
|
+
const seededCount = await seedMemories(project, resolvedPath);
|
|
864
|
+
console.log(`Seeded ${seededCount} initial memories from project scan.`);
|
|
865
|
+
|
|
866
|
+
const backendOk = await verifyBackendConnection(project);
|
|
867
|
+
|
|
868
|
+
console.log("");
|
|
869
|
+
console.log("✓ Git hook installed");
|
|
870
|
+
console.log("✓ MCP server registered");
|
|
871
|
+
console.log("✓ CLAUDE.md written");
|
|
872
|
+
console.log(`✓ Seeded ${seededCount} memories from project scan`);
|
|
873
|
+
if (backendOk) {
|
|
874
|
+
console.log("✓ Backend connection verified");
|
|
875
|
+
console.log("");
|
|
876
|
+
console.log(
|
|
877
|
+
`stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`
|
|
878
|
+
);
|
|
879
|
+
} else {
|
|
880
|
+
console.log("✗ Backend connection failed — run 'cm doctor' (once we build it)");
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Written last and deliberately: if a commit (and hence the post-commit
|
|
884
|
+
// hook) fires anywhere during init, it must still see whatever project
|
|
885
|
+
// .stackmem pointed to before this run, not the one being initialized now.
|
|
886
|
+
writeProjectFile(project, resolvedPath);
|
|
732
887
|
}
|
|
733
888
|
|
|
734
889
|
// --- cm context ------------------------------------------------------------
|
|
@@ -794,8 +949,9 @@ async function main() {
|
|
|
794
949
|
|
|
795
950
|
switch (command) {
|
|
796
951
|
case "start": {
|
|
797
|
-
const
|
|
798
|
-
if (!
|
|
952
|
+
const split = splitOptionalProject(args, 0);
|
|
953
|
+
if (!split) usage();
|
|
954
|
+
const project = requireProject(split.project);
|
|
799
955
|
await cmdStart(project);
|
|
800
956
|
break;
|
|
801
957
|
}
|
|
@@ -805,44 +961,66 @@ async function main() {
|
|
|
805
961
|
const positional = force
|
|
806
962
|
? [...args.slice(0, forceIndex), ...args.slice(forceIndex + 1)]
|
|
807
963
|
: args;
|
|
808
|
-
|
|
809
|
-
|
|
964
|
+
|
|
965
|
+
let explicitProject: string | null;
|
|
966
|
+
let type: string | undefined;
|
|
967
|
+
let rest: string[];
|
|
968
|
+
if (MEMORY_TYPES.includes(positional[0] as MemoryType)) {
|
|
969
|
+
explicitProject = null;
|
|
970
|
+
[type, ...rest] = positional;
|
|
971
|
+
} else {
|
|
972
|
+
explicitProject = positional[0] ?? null;
|
|
973
|
+
[, type, ...rest] = positional;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (!type || rest.length === 0) usage();
|
|
977
|
+
const project = requireProject(explicitProject);
|
|
810
978
|
await cmdSave(project, type, rest.join(" "), force);
|
|
811
979
|
break;
|
|
812
980
|
}
|
|
813
981
|
case "fix": {
|
|
814
|
-
const
|
|
815
|
-
if (!
|
|
982
|
+
const split = splitOptionalProject(args, 2);
|
|
983
|
+
if (!split) usage();
|
|
984
|
+
const project = requireProject(split.project);
|
|
985
|
+
const [problem, solution] = split.rest;
|
|
816
986
|
await cmdFix(project, problem, solution);
|
|
817
987
|
break;
|
|
818
988
|
}
|
|
819
989
|
case "analyze": {
|
|
820
|
-
const
|
|
821
|
-
if (!
|
|
990
|
+
const split = splitOptionalProject(args, 1);
|
|
991
|
+
if (!split) usage();
|
|
992
|
+
const project = requireProject(split.project);
|
|
993
|
+
const [path] = split.rest;
|
|
822
994
|
await cmdAnalyze(project, path);
|
|
823
995
|
break;
|
|
824
996
|
}
|
|
825
997
|
case "search": {
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
998
|
+
const split = splitProjectFromVariadic(args);
|
|
999
|
+
const project = requireProject(split.project);
|
|
1000
|
+
if (split.rest.length === 0) usage();
|
|
1001
|
+
await cmdSearch(project, split.rest.join(" "));
|
|
829
1002
|
break;
|
|
830
1003
|
}
|
|
831
1004
|
case "resolve": {
|
|
832
|
-
const
|
|
833
|
-
if (!
|
|
1005
|
+
const split = splitOptionalProject(args, 1);
|
|
1006
|
+
if (!split) usage();
|
|
1007
|
+
const project = requireProject(split.project);
|
|
1008
|
+
const [memoryId] = split.rest;
|
|
834
1009
|
await cmdResolve(project, memoryId);
|
|
835
1010
|
break;
|
|
836
1011
|
}
|
|
837
1012
|
case "delete": {
|
|
838
|
-
const
|
|
839
|
-
if (!
|
|
1013
|
+
const split = splitOptionalProject(args, 1);
|
|
1014
|
+
if (!split) usage();
|
|
1015
|
+
const project = requireProject(split.project);
|
|
1016
|
+
const [memoryId] = split.rest;
|
|
840
1017
|
await cmdDelete(project, memoryId);
|
|
841
1018
|
break;
|
|
842
1019
|
}
|
|
843
1020
|
case "compress": {
|
|
844
|
-
const
|
|
845
|
-
if (!
|
|
1021
|
+
const split = splitOptionalProject(args, 0);
|
|
1022
|
+
if (!split) usage();
|
|
1023
|
+
const project = requireProject(split.project);
|
|
846
1024
|
await cmdCompress(project);
|
|
847
1025
|
break;
|
|
848
1026
|
}
|
|
@@ -853,9 +1031,10 @@ async function main() {
|
|
|
853
1031
|
break;
|
|
854
1032
|
}
|
|
855
1033
|
case "context": {
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
1034
|
+
const split = splitProjectFromVariadic(args);
|
|
1035
|
+
const project = requireProject(split.project);
|
|
1036
|
+
if (split.rest.length === 0) usage();
|
|
1037
|
+
await cmdContext(project, split.rest.join(" "));
|
|
859
1038
|
break;
|
|
860
1039
|
}
|
|
861
1040
|
case "help": {
|