stackmem 1.0.3 → 1.0.5
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 +274 -58
- package/package.json +1 -1
- package/src/cli.ts +294 -58
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) {
|
|
@@ -430,21 +483,60 @@ function isSchemaFile(filePath) {
|
|
|
430
483
|
);
|
|
431
484
|
}
|
|
432
485
|
|
|
486
|
+
const LOW_QUALITY_PREFIXES = [
|
|
487
|
+
"wip", "fix", "update", "misc", "temp", "test", "patch", "minor",
|
|
488
|
+
"tweak", "change", "stuff", "done", "commit", "save", "ok", "m", "x",
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
// Filters out lazy/placeholder commit messages ("wip", "fix", "m", "...")
|
|
492
|
+
// so only messages worth remembering become decisions. Checked against the
|
|
493
|
+
// first word, not a raw prefix match, so a real message like "Migrate auth
|
|
494
|
+
// to JWT" isn't rejected just for starting with the letter "m".
|
|
495
|
+
function isQualityCommitMessage(message) {
|
|
496
|
+
const trimmed = message.trim();
|
|
497
|
+
if (trimmed.length <= 20) return false;
|
|
498
|
+
if (!trimmed.includes(" ")) return false;
|
|
499
|
+
|
|
500
|
+
const stripped = trimmed.replace(/\\s/g, "");
|
|
501
|
+
if (stripped.length > 0 && new Set(stripped).size === 1) return false;
|
|
502
|
+
|
|
503
|
+
const lowerTrimmed = trimmed.toLowerCase();
|
|
504
|
+
if (LOW_QUALITY_PREFIXES.includes(lowerTrimmed)) return false;
|
|
505
|
+
|
|
506
|
+
const firstWord = (lowerTrimmed.match(/^[a-z0-9]+/) || [""])[0];
|
|
507
|
+
if (LOW_QUALITY_PREFIXES.includes(firstWord)) return false;
|
|
508
|
+
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
|
|
433
512
|
const savedItems = [];
|
|
434
513
|
|
|
435
514
|
const commitMessage = git(["log", "-1", "--pretty=%B"]);
|
|
436
|
-
cmSave("decision", commitMessage);
|
|
437
|
-
savedItems.push(\`decision: \${commitMessage}\`);
|
|
438
515
|
|
|
439
|
-
const
|
|
516
|
+
const isFirstCommit = (() => {
|
|
517
|
+
try {
|
|
518
|
+
execFileSync("git", ["rev-parse", "HEAD~1"], { stdio: "pipe" });
|
|
519
|
+
return false;
|
|
520
|
+
} catch {
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
})();
|
|
524
|
+
|
|
525
|
+
const changedFiles = git(
|
|
526
|
+
isFirstCommit
|
|
527
|
+
? ["diff-tree", "--root", "--no-commit-id", "-r", "--name-only", "HEAD"]
|
|
528
|
+
: ["diff-tree", "--no-commit-id", "-r", "--name-only", "HEAD"]
|
|
529
|
+
)
|
|
440
530
|
.split("\\n")
|
|
441
531
|
.filter(Boolean);
|
|
442
532
|
|
|
443
533
|
let diff = "";
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
534
|
+
if (!isFirstCommit) {
|
|
535
|
+
try {
|
|
536
|
+
diff = execFileSync("git", ["diff", "HEAD~1", "HEAD"], { encoding: "utf-8" });
|
|
537
|
+
} catch {
|
|
538
|
+
diff = "";
|
|
539
|
+
}
|
|
448
540
|
}
|
|
449
541
|
|
|
450
542
|
const lines = diff.split("\\n");
|
|
@@ -452,7 +544,7 @@ let currentFile = null;
|
|
|
452
544
|
let isNewFile = false;
|
|
453
545
|
let inDependenciesSection = false;
|
|
454
546
|
|
|
455
|
-
for (const line of lines) {
|
|
547
|
+
for (const line of isFirstCommit ? [] : lines) {
|
|
456
548
|
if (line.startsWith("diff --git")) {
|
|
457
549
|
isNewFile = false;
|
|
458
550
|
inDependenciesSection = false;
|
|
@@ -502,10 +594,20 @@ for (const line of lines) {
|
|
|
502
594
|
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
503
595
|
const codeLine = line.slice(1).trim();
|
|
504
596
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
597
|
+
const importMatch = line.match(
|
|
598
|
+
/^-\\s*(import\\s+.*\\s+from\\s+['"]([^'"]+)['"]|import\\s+['"]([^'"]+)['"])/
|
|
599
|
+
);
|
|
600
|
+
const requireMatch = line.match(
|
|
601
|
+
/^-\\s*(?:const|let|var)\\s+\\S+\\s*=\\s*require\\(['"]([^'"]+)['"]\\)/
|
|
602
|
+
);
|
|
603
|
+
const removedModulePath = importMatch
|
|
604
|
+
? importMatch[2] || importMatch[3]
|
|
605
|
+
: requireMatch
|
|
606
|
+
? requireMatch[1]
|
|
607
|
+
: null;
|
|
608
|
+
|
|
609
|
+
if (removedModulePath) {
|
|
610
|
+
const content = \`removed dependency: \${removedModulePath}\`;
|
|
509
611
|
cmSave("rejection", content);
|
|
510
612
|
savedItems.push(\`rejection: \${content}\`);
|
|
511
613
|
}
|
|
@@ -561,6 +663,14 @@ for (const line of lines) {
|
|
|
561
663
|
}
|
|
562
664
|
}
|
|
563
665
|
|
|
666
|
+
// Saved last, after every diff-detected memory, and only if it clears the
|
|
667
|
+
// quality filter — diff analysis always runs regardless of commit message.
|
|
668
|
+
if (isQualityCommitMessage(commitMessage)) {
|
|
669
|
+
const trimmedMessage = commitMessage.trim();
|
|
670
|
+
cmSave("decision", trimmedMessage);
|
|
671
|
+
savedItems.push(\`decision: \${trimmedMessage}\`);
|
|
672
|
+
}
|
|
673
|
+
|
|
564
674
|
console.log(\`coding-memory post-commit summary (\${changedFiles.length} file(s) changed):\`);
|
|
565
675
|
for (const item of savedItems) {
|
|
566
676
|
console.log(\` - \${item}\`);
|
|
@@ -611,6 +721,69 @@ cm save ${project} <type> "<content>"
|
|
|
611
721
|
nodeFs.writeFileSync(claudeMdPath, content, "utf-8");
|
|
612
722
|
console.log("CLAUDE.md written.");
|
|
613
723
|
}
|
|
724
|
+
function writeProjectFile(project, resolvedPath) {
|
|
725
|
+
const stackmemPath = nodePath.join(resolvedPath, STACKMEM_FILE);
|
|
726
|
+
nodeFs.writeFileSync(stackmemPath, `${project}\n`, "utf-8");
|
|
727
|
+
}
|
|
728
|
+
// Scans the freshly-initialized project for a few cheap, high-signal facts
|
|
729
|
+
// and saves them as memories directly (bypassing cmdSave's duplicate/conflict
|
|
730
|
+
// prompt — there's nothing to conflict with yet, and init must stay
|
|
731
|
+
// non-interactive). Returns how many memories were actually saved.
|
|
732
|
+
async function seedMemories(project, resolvedPath) {
|
|
733
|
+
let seededCount = 0;
|
|
734
|
+
const packageJsonPath = nodePath.join(resolvedPath, "package.json");
|
|
735
|
+
if (nodeFs.existsSync(packageJsonPath)) {
|
|
736
|
+
try {
|
|
737
|
+
const pkg = JSON.parse(nodeFs.readFileSync(packageJsonPath, "utf-8"));
|
|
738
|
+
const depNames = [
|
|
739
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
740
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
741
|
+
];
|
|
742
|
+
if (depNames.length > 0) {
|
|
743
|
+
const { error } = await supabase.from("memories").insert({
|
|
744
|
+
project,
|
|
745
|
+
type: "discovery",
|
|
746
|
+
content: `tech stack: ${depNames.join(", ")}`,
|
|
747
|
+
device_id: getDeviceId(),
|
|
748
|
+
});
|
|
749
|
+
if (!error)
|
|
750
|
+
seededCount++;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
// Malformed package.json — skip the tech-stack memory.
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const envExamplePath = nodePath.join(resolvedPath, ".env.example");
|
|
758
|
+
if (nodeFs.existsSync(envExamplePath)) {
|
|
759
|
+
const varNames = nodeFs
|
|
760
|
+
.readFileSync(envExamplePath, "utf-8")
|
|
761
|
+
.split("\n")
|
|
762
|
+
.filter((line) => /^[A-Z_]+=/.test(line))
|
|
763
|
+
.map((line) => line.split("=")[0]);
|
|
764
|
+
if (varNames.length > 0) {
|
|
765
|
+
const { error } = await supabase.from("memories").insert({
|
|
766
|
+
project,
|
|
767
|
+
type: "constraint",
|
|
768
|
+
content: `required env vars: ${varNames.join(", ")}`,
|
|
769
|
+
device_id: getDeviceId(),
|
|
770
|
+
});
|
|
771
|
+
if (!error)
|
|
772
|
+
seededCount++;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
analyzeCodebase(resolvedPath);
|
|
776
|
+
return seededCount;
|
|
777
|
+
}
|
|
778
|
+
// Smoke-tests the same anon-key + device-header path session_start/cmdStart
|
|
779
|
+
// use, without pulling in all of cmdStart's scoring/formatting logic.
|
|
780
|
+
async function verifyBackendConnection(project) {
|
|
781
|
+
const { error } = await supabase
|
|
782
|
+
.from("memories")
|
|
783
|
+
.select("id", { count: "exact", head: true })
|
|
784
|
+
.eq("project", project);
|
|
785
|
+
return !error;
|
|
786
|
+
}
|
|
614
787
|
async function cmdInit(project, targetPath) {
|
|
615
788
|
const resolvedPath = nodePath.resolve(targetPath);
|
|
616
789
|
const gitDir = nodePath.join(resolvedPath, ".git");
|
|
@@ -621,7 +794,7 @@ async function cmdInit(project, targetPath) {
|
|
|
621
794
|
const codingMemoryDir = nodePath.join(resolvedPath, ".coding-memory");
|
|
622
795
|
nodeFs.mkdirSync(codingMemoryDir, { recursive: true });
|
|
623
796
|
const hookLogicPath = nodePath.join(codingMemoryDir, "post-commit-hook.mjs");
|
|
624
|
-
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(
|
|
797
|
+
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(DIST_CLI_PATH), "utf-8");
|
|
625
798
|
const hooksDir = nodePath.join(gitDir, "hooks");
|
|
626
799
|
nodeFs.mkdirSync(hooksDir, { recursive: true });
|
|
627
800
|
const hookPath = nodePath.join(hooksDir, "post-commit");
|
|
@@ -630,6 +803,26 @@ async function cmdInit(project, targetPath) {
|
|
|
630
803
|
console.log(`coding-memory hook installed for project ${project} at ${targetPath}`);
|
|
631
804
|
registerMcpServer(DIST_INDEX_PATH);
|
|
632
805
|
writeClaudeMd(project, resolvedPath);
|
|
806
|
+
const seededCount = await seedMemories(project, resolvedPath);
|
|
807
|
+
console.log(`Seeded ${seededCount} initial memories from project scan.`);
|
|
808
|
+
const backendOk = await verifyBackendConnection(project);
|
|
809
|
+
console.log("");
|
|
810
|
+
console.log("✓ Git hook installed");
|
|
811
|
+
console.log("✓ MCP server registered");
|
|
812
|
+
console.log("✓ CLAUDE.md written");
|
|
813
|
+
console.log(`✓ Seeded ${seededCount} memories from project scan`);
|
|
814
|
+
if (backendOk) {
|
|
815
|
+
console.log("✓ Backend connection verified");
|
|
816
|
+
console.log("");
|
|
817
|
+
console.log(`stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`);
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
console.log("✗ Backend connection failed — run 'cm doctor' (once we build it)");
|
|
821
|
+
}
|
|
822
|
+
// Written last and deliberately: if a commit (and hence the post-commit
|
|
823
|
+
// hook) fires anywhere during init, it must still see whatever project
|
|
824
|
+
// .stackmem pointed to before this run, not the one being initialized now.
|
|
825
|
+
writeProjectFile(project, resolvedPath);
|
|
633
826
|
}
|
|
634
827
|
// --- cm context ------------------------------------------------------------
|
|
635
828
|
const CONTEXT_TYPE_ORDER = ["decision", "constraint", "rejection", "discovery"];
|
|
@@ -679,9 +872,10 @@ async function main() {
|
|
|
679
872
|
const [, , command, ...args] = process.argv;
|
|
680
873
|
switch (command) {
|
|
681
874
|
case "start": {
|
|
682
|
-
const
|
|
683
|
-
if (!
|
|
875
|
+
const split = splitOptionalProject(args, 0);
|
|
876
|
+
if (!split)
|
|
684
877
|
usage();
|
|
878
|
+
const project = requireProject(split.project);
|
|
685
879
|
await cmdStart(project);
|
|
686
880
|
break;
|
|
687
881
|
}
|
|
@@ -691,51 +885,72 @@ async function main() {
|
|
|
691
885
|
const positional = force
|
|
692
886
|
? [...args.slice(0, forceIndex), ...args.slice(forceIndex + 1)]
|
|
693
887
|
: args;
|
|
694
|
-
|
|
695
|
-
|
|
888
|
+
let explicitProject;
|
|
889
|
+
let type;
|
|
890
|
+
let rest;
|
|
891
|
+
if (MEMORY_TYPES.includes(positional[0])) {
|
|
892
|
+
explicitProject = null;
|
|
893
|
+
[type, ...rest] = positional;
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
explicitProject = positional[0] ?? null;
|
|
897
|
+
[, type, ...rest] = positional;
|
|
898
|
+
}
|
|
899
|
+
if (!type || rest.length === 0)
|
|
696
900
|
usage();
|
|
901
|
+
const project = requireProject(explicitProject);
|
|
697
902
|
await cmdSave(project, type, rest.join(" "), force);
|
|
698
903
|
break;
|
|
699
904
|
}
|
|
700
905
|
case "fix": {
|
|
701
|
-
const
|
|
702
|
-
if (!
|
|
906
|
+
const split = splitOptionalProject(args, 2);
|
|
907
|
+
if (!split)
|
|
703
908
|
usage();
|
|
909
|
+
const project = requireProject(split.project);
|
|
910
|
+
const [problem, solution] = split.rest;
|
|
704
911
|
await cmdFix(project, problem, solution);
|
|
705
912
|
break;
|
|
706
913
|
}
|
|
707
914
|
case "analyze": {
|
|
708
|
-
const
|
|
709
|
-
if (!
|
|
915
|
+
const split = splitOptionalProject(args, 1);
|
|
916
|
+
if (!split)
|
|
710
917
|
usage();
|
|
918
|
+
const project = requireProject(split.project);
|
|
919
|
+
const [path] = split.rest;
|
|
711
920
|
await cmdAnalyze(project, path);
|
|
712
921
|
break;
|
|
713
922
|
}
|
|
714
923
|
case "search": {
|
|
715
|
-
const
|
|
716
|
-
|
|
924
|
+
const split = splitProjectFromVariadic(args);
|
|
925
|
+
const project = requireProject(split.project);
|
|
926
|
+
if (split.rest.length === 0)
|
|
717
927
|
usage();
|
|
718
|
-
await cmdSearch(project, rest.join(" "));
|
|
928
|
+
await cmdSearch(project, split.rest.join(" "));
|
|
719
929
|
break;
|
|
720
930
|
}
|
|
721
931
|
case "resolve": {
|
|
722
|
-
const
|
|
723
|
-
if (!
|
|
932
|
+
const split = splitOptionalProject(args, 1);
|
|
933
|
+
if (!split)
|
|
724
934
|
usage();
|
|
935
|
+
const project = requireProject(split.project);
|
|
936
|
+
const [memoryId] = split.rest;
|
|
725
937
|
await cmdResolve(project, memoryId);
|
|
726
938
|
break;
|
|
727
939
|
}
|
|
728
940
|
case "delete": {
|
|
729
|
-
const
|
|
730
|
-
if (!
|
|
941
|
+
const split = splitOptionalProject(args, 1);
|
|
942
|
+
if (!split)
|
|
731
943
|
usage();
|
|
944
|
+
const project = requireProject(split.project);
|
|
945
|
+
const [memoryId] = split.rest;
|
|
732
946
|
await cmdDelete(project, memoryId);
|
|
733
947
|
break;
|
|
734
948
|
}
|
|
735
949
|
case "compress": {
|
|
736
|
-
const
|
|
737
|
-
if (!
|
|
950
|
+
const split = splitOptionalProject(args, 0);
|
|
951
|
+
if (!split)
|
|
738
952
|
usage();
|
|
953
|
+
const project = requireProject(split.project);
|
|
739
954
|
await cmdCompress(project);
|
|
740
955
|
break;
|
|
741
956
|
}
|
|
@@ -747,10 +962,11 @@ async function main() {
|
|
|
747
962
|
break;
|
|
748
963
|
}
|
|
749
964
|
case "context": {
|
|
750
|
-
const
|
|
751
|
-
|
|
965
|
+
const split = splitProjectFromVariadic(args);
|
|
966
|
+
const project = requireProject(split.project);
|
|
967
|
+
if (split.rest.length === 0)
|
|
752
968
|
usage();
|
|
753
|
-
await cmdContext(project, rest.join(" "));
|
|
969
|
+
await cmdContext(project, split.rest.join(" "));
|
|
754
970
|
break;
|
|
755
971
|
}
|
|
756
972
|
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) {
|
|
@@ -512,21 +573,60 @@ function isSchemaFile(filePath) {
|
|
|
512
573
|
);
|
|
513
574
|
}
|
|
514
575
|
|
|
576
|
+
const LOW_QUALITY_PREFIXES = [
|
|
577
|
+
"wip", "fix", "update", "misc", "temp", "test", "patch", "minor",
|
|
578
|
+
"tweak", "change", "stuff", "done", "commit", "save", "ok", "m", "x",
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
// Filters out lazy/placeholder commit messages ("wip", "fix", "m", "...")
|
|
582
|
+
// so only messages worth remembering become decisions. Checked against the
|
|
583
|
+
// first word, not a raw prefix match, so a real message like "Migrate auth
|
|
584
|
+
// to JWT" isn't rejected just for starting with the letter "m".
|
|
585
|
+
function isQualityCommitMessage(message) {
|
|
586
|
+
const trimmed = message.trim();
|
|
587
|
+
if (trimmed.length <= 20) return false;
|
|
588
|
+
if (!trimmed.includes(" ")) return false;
|
|
589
|
+
|
|
590
|
+
const stripped = trimmed.replace(/\\s/g, "");
|
|
591
|
+
if (stripped.length > 0 && new Set(stripped).size === 1) return false;
|
|
592
|
+
|
|
593
|
+
const lowerTrimmed = trimmed.toLowerCase();
|
|
594
|
+
if (LOW_QUALITY_PREFIXES.includes(lowerTrimmed)) return false;
|
|
595
|
+
|
|
596
|
+
const firstWord = (lowerTrimmed.match(/^[a-z0-9]+/) || [""])[0];
|
|
597
|
+
if (LOW_QUALITY_PREFIXES.includes(firstWord)) return false;
|
|
598
|
+
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
|
|
515
602
|
const savedItems = [];
|
|
516
603
|
|
|
517
604
|
const commitMessage = git(["log", "-1", "--pretty=%B"]);
|
|
518
|
-
cmSave("decision", commitMessage);
|
|
519
|
-
savedItems.push(\`decision: \${commitMessage}\`);
|
|
520
605
|
|
|
521
|
-
const
|
|
606
|
+
const isFirstCommit = (() => {
|
|
607
|
+
try {
|
|
608
|
+
execFileSync("git", ["rev-parse", "HEAD~1"], { stdio: "pipe" });
|
|
609
|
+
return false;
|
|
610
|
+
} catch {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
})();
|
|
614
|
+
|
|
615
|
+
const changedFiles = git(
|
|
616
|
+
isFirstCommit
|
|
617
|
+
? ["diff-tree", "--root", "--no-commit-id", "-r", "--name-only", "HEAD"]
|
|
618
|
+
: ["diff-tree", "--no-commit-id", "-r", "--name-only", "HEAD"]
|
|
619
|
+
)
|
|
522
620
|
.split("\\n")
|
|
523
621
|
.filter(Boolean);
|
|
524
622
|
|
|
525
623
|
let diff = "";
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
624
|
+
if (!isFirstCommit) {
|
|
625
|
+
try {
|
|
626
|
+
diff = execFileSync("git", ["diff", "HEAD~1", "HEAD"], { encoding: "utf-8" });
|
|
627
|
+
} catch {
|
|
628
|
+
diff = "";
|
|
629
|
+
}
|
|
530
630
|
}
|
|
531
631
|
|
|
532
632
|
const lines = diff.split("\\n");
|
|
@@ -534,7 +634,7 @@ let currentFile = null;
|
|
|
534
634
|
let isNewFile = false;
|
|
535
635
|
let inDependenciesSection = false;
|
|
536
636
|
|
|
537
|
-
for (const line of lines) {
|
|
637
|
+
for (const line of isFirstCommit ? [] : lines) {
|
|
538
638
|
if (line.startsWith("diff --git")) {
|
|
539
639
|
isNewFile = false;
|
|
540
640
|
inDependenciesSection = false;
|
|
@@ -584,10 +684,20 @@ for (const line of lines) {
|
|
|
584
684
|
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
585
685
|
const codeLine = line.slice(1).trim();
|
|
586
686
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
687
|
+
const importMatch = line.match(
|
|
688
|
+
/^-\\s*(import\\s+.*\\s+from\\s+['"]([^'"]+)['"]|import\\s+['"]([^'"]+)['"])/
|
|
689
|
+
);
|
|
690
|
+
const requireMatch = line.match(
|
|
691
|
+
/^-\\s*(?:const|let|var)\\s+\\S+\\s*=\\s*require\\(['"]([^'"]+)['"]\\)/
|
|
692
|
+
);
|
|
693
|
+
const removedModulePath = importMatch
|
|
694
|
+
? importMatch[2] || importMatch[3]
|
|
695
|
+
: requireMatch
|
|
696
|
+
? requireMatch[1]
|
|
697
|
+
: null;
|
|
698
|
+
|
|
699
|
+
if (removedModulePath) {
|
|
700
|
+
const content = \`removed dependency: \${removedModulePath}\`;
|
|
591
701
|
cmSave("rejection", content);
|
|
592
702
|
savedItems.push(\`rejection: \${content}\`);
|
|
593
703
|
}
|
|
@@ -643,6 +753,14 @@ for (const line of lines) {
|
|
|
643
753
|
}
|
|
644
754
|
}
|
|
645
755
|
|
|
756
|
+
// Saved last, after every diff-detected memory, and only if it clears the
|
|
757
|
+
// quality filter — diff analysis always runs regardless of commit message.
|
|
758
|
+
if (isQualityCommitMessage(commitMessage)) {
|
|
759
|
+
const trimmedMessage = commitMessage.trim();
|
|
760
|
+
cmSave("decision", trimmedMessage);
|
|
761
|
+
savedItems.push(\`decision: \${trimmedMessage}\`);
|
|
762
|
+
}
|
|
763
|
+
|
|
646
764
|
console.log(\`coding-memory post-commit summary (\${changedFiles.length} file(s) changed):\`);
|
|
647
765
|
for (const item of savedItems) {
|
|
648
766
|
console.log(\` - \${item}\`);
|
|
@@ -703,6 +821,75 @@ cm save ${project} <type> "<content>"
|
|
|
703
821
|
console.log("CLAUDE.md written.");
|
|
704
822
|
}
|
|
705
823
|
|
|
824
|
+
function writeProjectFile(project: string, resolvedPath: string): void {
|
|
825
|
+
const stackmemPath = nodePath.join(resolvedPath, STACKMEM_FILE);
|
|
826
|
+
nodeFs.writeFileSync(stackmemPath, `${project}\n`, "utf-8");
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Scans the freshly-initialized project for a few cheap, high-signal facts
|
|
830
|
+
// and saves them as memories directly (bypassing cmdSave's duplicate/conflict
|
|
831
|
+
// prompt — there's nothing to conflict with yet, and init must stay
|
|
832
|
+
// non-interactive). Returns how many memories were actually saved.
|
|
833
|
+
async function seedMemories(project: string, resolvedPath: string): Promise<number> {
|
|
834
|
+
let seededCount = 0;
|
|
835
|
+
|
|
836
|
+
const packageJsonPath = nodePath.join(resolvedPath, "package.json");
|
|
837
|
+
if (nodeFs.existsSync(packageJsonPath)) {
|
|
838
|
+
try {
|
|
839
|
+
const pkg = JSON.parse(nodeFs.readFileSync(packageJsonPath, "utf-8"));
|
|
840
|
+
const depNames = [
|
|
841
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
842
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
843
|
+
];
|
|
844
|
+
if (depNames.length > 0) {
|
|
845
|
+
const { error } = await supabase.from("memories").insert({
|
|
846
|
+
project,
|
|
847
|
+
type: "discovery",
|
|
848
|
+
content: `tech stack: ${depNames.join(", ")}`,
|
|
849
|
+
device_id: getDeviceId(),
|
|
850
|
+
});
|
|
851
|
+
if (!error) seededCount++;
|
|
852
|
+
}
|
|
853
|
+
} catch {
|
|
854
|
+
// Malformed package.json — skip the tech-stack memory.
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const envExamplePath = nodePath.join(resolvedPath, ".env.example");
|
|
859
|
+
if (nodeFs.existsSync(envExamplePath)) {
|
|
860
|
+
const varNames = nodeFs
|
|
861
|
+
.readFileSync(envExamplePath, "utf-8")
|
|
862
|
+
.split("\n")
|
|
863
|
+
.filter((line) => /^[A-Z_]+=/.test(line))
|
|
864
|
+
.map((line) => line.split("=")[0]);
|
|
865
|
+
|
|
866
|
+
if (varNames.length > 0) {
|
|
867
|
+
const { error } = await supabase.from("memories").insert({
|
|
868
|
+
project,
|
|
869
|
+
type: "constraint",
|
|
870
|
+
content: `required env vars: ${varNames.join(", ")}`,
|
|
871
|
+
device_id: getDeviceId(),
|
|
872
|
+
});
|
|
873
|
+
if (!error) seededCount++;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
analyzeCodebase(resolvedPath);
|
|
878
|
+
|
|
879
|
+
return seededCount;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Smoke-tests the same anon-key + device-header path session_start/cmdStart
|
|
883
|
+
// use, without pulling in all of cmdStart's scoring/formatting logic.
|
|
884
|
+
async function verifyBackendConnection(project: string): Promise<boolean> {
|
|
885
|
+
const { error } = await supabase
|
|
886
|
+
.from("memories")
|
|
887
|
+
.select("id", { count: "exact", head: true })
|
|
888
|
+
.eq("project", project);
|
|
889
|
+
|
|
890
|
+
return !error;
|
|
891
|
+
}
|
|
892
|
+
|
|
706
893
|
async function cmdInit(project: string, targetPath: string) {
|
|
707
894
|
const resolvedPath = nodePath.resolve(targetPath);
|
|
708
895
|
const gitDir = nodePath.join(resolvedPath, ".git");
|
|
@@ -716,7 +903,7 @@ async function cmdInit(project: string, targetPath: string) {
|
|
|
716
903
|
nodeFs.mkdirSync(codingMemoryDir, { recursive: true });
|
|
717
904
|
|
|
718
905
|
const hookLogicPath = nodePath.join(codingMemoryDir, "post-commit-hook.mjs");
|
|
719
|
-
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(
|
|
906
|
+
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(DIST_CLI_PATH), "utf-8");
|
|
720
907
|
|
|
721
908
|
const hooksDir = nodePath.join(gitDir, "hooks");
|
|
722
909
|
nodeFs.mkdirSync(hooksDir, { recursive: true });
|
|
@@ -729,6 +916,31 @@ async function cmdInit(project: string, targetPath: string) {
|
|
|
729
916
|
|
|
730
917
|
registerMcpServer(DIST_INDEX_PATH);
|
|
731
918
|
writeClaudeMd(project, resolvedPath);
|
|
919
|
+
|
|
920
|
+
const seededCount = await seedMemories(project, resolvedPath);
|
|
921
|
+
console.log(`Seeded ${seededCount} initial memories from project scan.`);
|
|
922
|
+
|
|
923
|
+
const backendOk = await verifyBackendConnection(project);
|
|
924
|
+
|
|
925
|
+
console.log("");
|
|
926
|
+
console.log("✓ Git hook installed");
|
|
927
|
+
console.log("✓ MCP server registered");
|
|
928
|
+
console.log("✓ CLAUDE.md written");
|
|
929
|
+
console.log(`✓ Seeded ${seededCount} memories from project scan`);
|
|
930
|
+
if (backendOk) {
|
|
931
|
+
console.log("✓ Backend connection verified");
|
|
932
|
+
console.log("");
|
|
933
|
+
console.log(
|
|
934
|
+
`stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`
|
|
935
|
+
);
|
|
936
|
+
} else {
|
|
937
|
+
console.log("✗ Backend connection failed — run 'cm doctor' (once we build it)");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Written last and deliberately: if a commit (and hence the post-commit
|
|
941
|
+
// hook) fires anywhere during init, it must still see whatever project
|
|
942
|
+
// .stackmem pointed to before this run, not the one being initialized now.
|
|
943
|
+
writeProjectFile(project, resolvedPath);
|
|
732
944
|
}
|
|
733
945
|
|
|
734
946
|
// --- cm context ------------------------------------------------------------
|
|
@@ -794,8 +1006,9 @@ async function main() {
|
|
|
794
1006
|
|
|
795
1007
|
switch (command) {
|
|
796
1008
|
case "start": {
|
|
797
|
-
const
|
|
798
|
-
if (!
|
|
1009
|
+
const split = splitOptionalProject(args, 0);
|
|
1010
|
+
if (!split) usage();
|
|
1011
|
+
const project = requireProject(split.project);
|
|
799
1012
|
await cmdStart(project);
|
|
800
1013
|
break;
|
|
801
1014
|
}
|
|
@@ -805,44 +1018,66 @@ async function main() {
|
|
|
805
1018
|
const positional = force
|
|
806
1019
|
? [...args.slice(0, forceIndex), ...args.slice(forceIndex + 1)]
|
|
807
1020
|
: args;
|
|
808
|
-
|
|
809
|
-
|
|
1021
|
+
|
|
1022
|
+
let explicitProject: string | null;
|
|
1023
|
+
let type: string | undefined;
|
|
1024
|
+
let rest: string[];
|
|
1025
|
+
if (MEMORY_TYPES.includes(positional[0] as MemoryType)) {
|
|
1026
|
+
explicitProject = null;
|
|
1027
|
+
[type, ...rest] = positional;
|
|
1028
|
+
} else {
|
|
1029
|
+
explicitProject = positional[0] ?? null;
|
|
1030
|
+
[, type, ...rest] = positional;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (!type || rest.length === 0) usage();
|
|
1034
|
+
const project = requireProject(explicitProject);
|
|
810
1035
|
await cmdSave(project, type, rest.join(" "), force);
|
|
811
1036
|
break;
|
|
812
1037
|
}
|
|
813
1038
|
case "fix": {
|
|
814
|
-
const
|
|
815
|
-
if (!
|
|
1039
|
+
const split = splitOptionalProject(args, 2);
|
|
1040
|
+
if (!split) usage();
|
|
1041
|
+
const project = requireProject(split.project);
|
|
1042
|
+
const [problem, solution] = split.rest;
|
|
816
1043
|
await cmdFix(project, problem, solution);
|
|
817
1044
|
break;
|
|
818
1045
|
}
|
|
819
1046
|
case "analyze": {
|
|
820
|
-
const
|
|
821
|
-
if (!
|
|
1047
|
+
const split = splitOptionalProject(args, 1);
|
|
1048
|
+
if (!split) usage();
|
|
1049
|
+
const project = requireProject(split.project);
|
|
1050
|
+
const [path] = split.rest;
|
|
822
1051
|
await cmdAnalyze(project, path);
|
|
823
1052
|
break;
|
|
824
1053
|
}
|
|
825
1054
|
case "search": {
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
1055
|
+
const split = splitProjectFromVariadic(args);
|
|
1056
|
+
const project = requireProject(split.project);
|
|
1057
|
+
if (split.rest.length === 0) usage();
|
|
1058
|
+
await cmdSearch(project, split.rest.join(" "));
|
|
829
1059
|
break;
|
|
830
1060
|
}
|
|
831
1061
|
case "resolve": {
|
|
832
|
-
const
|
|
833
|
-
if (!
|
|
1062
|
+
const split = splitOptionalProject(args, 1);
|
|
1063
|
+
if (!split) usage();
|
|
1064
|
+
const project = requireProject(split.project);
|
|
1065
|
+
const [memoryId] = split.rest;
|
|
834
1066
|
await cmdResolve(project, memoryId);
|
|
835
1067
|
break;
|
|
836
1068
|
}
|
|
837
1069
|
case "delete": {
|
|
838
|
-
const
|
|
839
|
-
if (!
|
|
1070
|
+
const split = splitOptionalProject(args, 1);
|
|
1071
|
+
if (!split) usage();
|
|
1072
|
+
const project = requireProject(split.project);
|
|
1073
|
+
const [memoryId] = split.rest;
|
|
840
1074
|
await cmdDelete(project, memoryId);
|
|
841
1075
|
break;
|
|
842
1076
|
}
|
|
843
1077
|
case "compress": {
|
|
844
|
-
const
|
|
845
|
-
if (!
|
|
1078
|
+
const split = splitOptionalProject(args, 0);
|
|
1079
|
+
if (!split) usage();
|
|
1080
|
+
const project = requireProject(split.project);
|
|
846
1081
|
await cmdCompress(project);
|
|
847
1082
|
break;
|
|
848
1083
|
}
|
|
@@ -853,9 +1088,10 @@ async function main() {
|
|
|
853
1088
|
break;
|
|
854
1089
|
}
|
|
855
1090
|
case "context": {
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
1091
|
+
const split = splitProjectFromVariadic(args);
|
|
1092
|
+
const project = requireProject(split.project);
|
|
1093
|
+
if (split.rest.length === 0) usage();
|
|
1094
|
+
await cmdContext(project, split.rest.join(" "));
|
|
859
1095
|
break;
|
|
860
1096
|
}
|
|
861
1097
|
case "help": {
|