stackmem 1.0.2 → 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 +253 -46
- package/package.json +1 -1
- package/src/cli.ts +283 -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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import nodeFs from "node:fs";
|
|
3
|
+
import nodeOs from "node:os";
|
|
3
4
|
import nodePath from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { createInterface } from "node:readline/promises";
|
|
@@ -14,6 +15,7 @@ const CLI_PATH = fileURLToPath(import.meta.url);
|
|
|
14
15
|
// directly — tsx's esbuild transform fails when invoked from inside a git
|
|
15
16
|
// hook's stripped-down shell environment.
|
|
16
17
|
const DIST_CLI_PATH = nodePath.join(nodePath.dirname(CLI_PATH), "..", "dist", "cli.js");
|
|
18
|
+
const DIST_INDEX_PATH = nodePath.join(nodePath.dirname(CLI_PATH), "..", "dist", "index.js");
|
|
17
19
|
const MEMORY_TYPES = ["decision", "rejection", "constraint", "discovery"];
|
|
18
20
|
const TYPE_LABELS = {
|
|
19
21
|
decision: "Decisions",
|
|
@@ -23,63 +25,68 @@ const TYPE_LABELS = {
|
|
|
23
25
|
};
|
|
24
26
|
function usage() {
|
|
25
27
|
console.error(`Usage:
|
|
26
|
-
cm start
|
|
27
|
-
cm save
|
|
28
|
-
cm fix
|
|
29
|
-
cm analyze
|
|
30
|
-
cm search
|
|
31
|
-
cm resolve
|
|
32
|
-
cm delete
|
|
33
|
-
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]
|
|
34
36
|
cm init <project> <path>
|
|
35
|
-
cm context
|
|
36
|
-
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.`);
|
|
37
42
|
process.exit(1);
|
|
38
43
|
}
|
|
39
44
|
function cmdHelp() {
|
|
40
45
|
console.log(`cm — coding memory CLI
|
|
41
46
|
|
|
42
|
-
|
|
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]
|
|
43
51
|
Load all unresolved memories for a project, sorted by decay
|
|
44
52
|
score and capped at a 2000 token context budget, grouped by
|
|
45
53
|
type (decisions, constraints, discoveries, rejections), with
|
|
46
54
|
linked memory ids shown under each entry.
|
|
47
55
|
|
|
48
|
-
cm save
|
|
56
|
+
cm save [project] <type> <content> [--force]
|
|
49
57
|
Save a new memory. <type> is one of: decision, rejection,
|
|
50
58
|
constraint, discovery. Automatically links to related existing
|
|
51
59
|
memories. If the new memory contradicts an existing one, prompts
|
|
52
60
|
to keep the old memory or replace it — pass --force to skip the
|
|
53
61
|
prompt and always replace.
|
|
54
62
|
|
|
55
|
-
cm fix
|
|
63
|
+
cm fix [project] <problem> <solution>
|
|
56
64
|
Record a problem and its solution for future reference.
|
|
57
65
|
|
|
58
|
-
cm analyze
|
|
66
|
+
cm analyze [project] <path>
|
|
59
67
|
Build an AST index (files, functions, classes) for the codebase
|
|
60
68
|
at <path>.
|
|
61
69
|
|
|
62
|
-
cm search
|
|
70
|
+
cm search [project] <query>
|
|
63
71
|
Search saved memories and past fixes for a project matching
|
|
64
72
|
<query>.
|
|
65
73
|
|
|
66
|
-
cm resolve
|
|
74
|
+
cm resolve [project] <memory-id>
|
|
67
75
|
Mark a memory as resolved.
|
|
68
76
|
|
|
69
|
-
cm delete
|
|
77
|
+
cm delete [project] <memory-id>
|
|
70
78
|
Delete a memory and any memory_links referencing it.
|
|
71
79
|
|
|
72
|
-
cm compress
|
|
80
|
+
cm compress [project]
|
|
73
81
|
Cluster related unresolved memories (needs 20+) and collapse
|
|
74
82
|
clusters of 3 or more into a single summary memory.
|
|
75
83
|
|
|
76
84
|
cm init <project> <path>
|
|
77
|
-
Install a git post-commit hook in the repo at <path
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
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.
|
|
81
88
|
|
|
82
|
-
cm context
|
|
89
|
+
cm context [project] <task>
|
|
83
90
|
Return only the top 5 memories most relevant to a specific
|
|
84
91
|
task, ranked by a blend of task relevance and decay score,
|
|
85
92
|
as a markdown block capped at a 2000 token budget.
|
|
@@ -91,6 +98,45 @@ function fail(message) {
|
|
|
91
98
|
console.error(`Error: ${message}`);
|
|
92
99
|
process.exit(1);
|
|
93
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
|
+
}
|
|
94
140
|
// --- cm start ----------------------------------------------------------
|
|
95
141
|
async function cmdStart(project) {
|
|
96
142
|
const { data, error } = await supabase
|
|
@@ -387,11 +433,20 @@ async function cmdDelete(project, memoryId) {
|
|
|
387
433
|
// .git/hooks/post-commit) so its module type is unambiguous — an
|
|
388
434
|
// extensionless file run by git would otherwise have its CommonJS/ESM
|
|
389
435
|
// interpretation depend on the target repo's own package.json.
|
|
390
|
-
function buildHookLogicScript(
|
|
436
|
+
function buildHookLogicScript(cliPath) {
|
|
391
437
|
return `#!/usr/bin/env node
|
|
392
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
|
+
}
|
|
393
449
|
|
|
394
|
-
const PROJECT = ${JSON.stringify(project)};
|
|
395
450
|
const CLI_PATH = ${JSON.stringify(cliPath)};
|
|
396
451
|
|
|
397
452
|
function git(args) {
|
|
@@ -565,6 +620,113 @@ for (const item of savedItems) {
|
|
|
565
620
|
}
|
|
566
621
|
`;
|
|
567
622
|
}
|
|
623
|
+
function registerMcpServer(distIndexPath) {
|
|
624
|
+
const claudeConfigPath = nodePath.join(nodeOs.homedir(), ".claude.json");
|
|
625
|
+
let config = {};
|
|
626
|
+
if (nodeFs.existsSync(claudeConfigPath)) {
|
|
627
|
+
try {
|
|
628
|
+
config = JSON.parse(nodeFs.readFileSync(claudeConfigPath, "utf-8"));
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
config = {};
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
635
|
+
config.mcpServers = {};
|
|
636
|
+
}
|
|
637
|
+
if (config.mcpServers.stackmem) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
config.mcpServers.stackmem = {
|
|
641
|
+
command: "node",
|
|
642
|
+
args: [distIndexPath],
|
|
643
|
+
};
|
|
644
|
+
nodeFs.writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), "utf-8");
|
|
645
|
+
console.log("stackmem MCP server registered with Claude Code.");
|
|
646
|
+
}
|
|
647
|
+
function writeClaudeMd(project, resolvedPath) {
|
|
648
|
+
const claudeMdPath = nodePath.join(resolvedPath, "CLAUDE.md");
|
|
649
|
+
if (nodeFs.existsSync(claudeMdPath)) {
|
|
650
|
+
console.log("CLAUDE.md already exists — skipping.");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const content = `# stackmem
|
|
654
|
+
|
|
655
|
+
At the start of every session, the stackmem MCP server will automatically load memories for this project. The project name is "${project}".
|
|
656
|
+
|
|
657
|
+
If the MCP server is not connected, run:
|
|
658
|
+
cm start ${project}
|
|
659
|
+
and paste the output here before starting work.
|
|
660
|
+
|
|
661
|
+
When you make a decision, discover a constraint, reject an approach, or make a discovery during this session, save it with:
|
|
662
|
+
cm save ${project} <type> "<content>"
|
|
663
|
+
`;
|
|
664
|
+
nodeFs.writeFileSync(claudeMdPath, content, "utf-8");
|
|
665
|
+
console.log("CLAUDE.md written.");
|
|
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
|
+
}
|
|
568
730
|
async function cmdInit(project, targetPath) {
|
|
569
731
|
const resolvedPath = nodePath.resolve(targetPath);
|
|
570
732
|
const gitDir = nodePath.join(resolvedPath, ".git");
|
|
@@ -575,13 +737,35 @@ async function cmdInit(project, targetPath) {
|
|
|
575
737
|
const codingMemoryDir = nodePath.join(resolvedPath, ".coding-memory");
|
|
576
738
|
nodeFs.mkdirSync(codingMemoryDir, { recursive: true });
|
|
577
739
|
const hookLogicPath = nodePath.join(codingMemoryDir, "post-commit-hook.mjs");
|
|
578
|
-
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(
|
|
740
|
+
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(DIST_CLI_PATH), "utf-8");
|
|
579
741
|
const hooksDir = nodePath.join(gitDir, "hooks");
|
|
580
742
|
nodeFs.mkdirSync(hooksDir, { recursive: true });
|
|
581
743
|
const hookPath = nodePath.join(hooksDir, "post-commit");
|
|
582
744
|
nodeFs.writeFileSync(hookPath, `#!/bin/sh\nexec node "${hookLogicPath}" "$@"\n`, "utf-8");
|
|
583
745
|
nodeFs.chmodSync(hookPath, 0o755);
|
|
584
746
|
console.log(`coding-memory hook installed for project ${project} at ${targetPath}`);
|
|
747
|
+
registerMcpServer(DIST_INDEX_PATH);
|
|
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);
|
|
585
769
|
}
|
|
586
770
|
// --- cm context ------------------------------------------------------------
|
|
587
771
|
const CONTEXT_TYPE_ORDER = ["decision", "constraint", "rejection", "discovery"];
|
|
@@ -631,9 +815,10 @@ async function main() {
|
|
|
631
815
|
const [, , command, ...args] = process.argv;
|
|
632
816
|
switch (command) {
|
|
633
817
|
case "start": {
|
|
634
|
-
const
|
|
635
|
-
if (!
|
|
818
|
+
const split = splitOptionalProject(args, 0);
|
|
819
|
+
if (!split)
|
|
636
820
|
usage();
|
|
821
|
+
const project = requireProject(split.project);
|
|
637
822
|
await cmdStart(project);
|
|
638
823
|
break;
|
|
639
824
|
}
|
|
@@ -643,51 +828,72 @@ async function main() {
|
|
|
643
828
|
const positional = force
|
|
644
829
|
? [...args.slice(0, forceIndex), ...args.slice(forceIndex + 1)]
|
|
645
830
|
: args;
|
|
646
|
-
|
|
647
|
-
|
|
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)
|
|
648
843
|
usage();
|
|
844
|
+
const project = requireProject(explicitProject);
|
|
649
845
|
await cmdSave(project, type, rest.join(" "), force);
|
|
650
846
|
break;
|
|
651
847
|
}
|
|
652
848
|
case "fix": {
|
|
653
|
-
const
|
|
654
|
-
if (!
|
|
849
|
+
const split = splitOptionalProject(args, 2);
|
|
850
|
+
if (!split)
|
|
655
851
|
usage();
|
|
852
|
+
const project = requireProject(split.project);
|
|
853
|
+
const [problem, solution] = split.rest;
|
|
656
854
|
await cmdFix(project, problem, solution);
|
|
657
855
|
break;
|
|
658
856
|
}
|
|
659
857
|
case "analyze": {
|
|
660
|
-
const
|
|
661
|
-
if (!
|
|
858
|
+
const split = splitOptionalProject(args, 1);
|
|
859
|
+
if (!split)
|
|
662
860
|
usage();
|
|
861
|
+
const project = requireProject(split.project);
|
|
862
|
+
const [path] = split.rest;
|
|
663
863
|
await cmdAnalyze(project, path);
|
|
664
864
|
break;
|
|
665
865
|
}
|
|
666
866
|
case "search": {
|
|
667
|
-
const
|
|
668
|
-
|
|
867
|
+
const split = splitProjectFromVariadic(args);
|
|
868
|
+
const project = requireProject(split.project);
|
|
869
|
+
if (split.rest.length === 0)
|
|
669
870
|
usage();
|
|
670
|
-
await cmdSearch(project, rest.join(" "));
|
|
871
|
+
await cmdSearch(project, split.rest.join(" "));
|
|
671
872
|
break;
|
|
672
873
|
}
|
|
673
874
|
case "resolve": {
|
|
674
|
-
const
|
|
675
|
-
if (!
|
|
875
|
+
const split = splitOptionalProject(args, 1);
|
|
876
|
+
if (!split)
|
|
676
877
|
usage();
|
|
878
|
+
const project = requireProject(split.project);
|
|
879
|
+
const [memoryId] = split.rest;
|
|
677
880
|
await cmdResolve(project, memoryId);
|
|
678
881
|
break;
|
|
679
882
|
}
|
|
680
883
|
case "delete": {
|
|
681
|
-
const
|
|
682
|
-
if (!
|
|
884
|
+
const split = splitOptionalProject(args, 1);
|
|
885
|
+
if (!split)
|
|
683
886
|
usage();
|
|
887
|
+
const project = requireProject(split.project);
|
|
888
|
+
const [memoryId] = split.rest;
|
|
684
889
|
await cmdDelete(project, memoryId);
|
|
685
890
|
break;
|
|
686
891
|
}
|
|
687
892
|
case "compress": {
|
|
688
|
-
const
|
|
689
|
-
if (!
|
|
893
|
+
const split = splitOptionalProject(args, 0);
|
|
894
|
+
if (!split)
|
|
690
895
|
usage();
|
|
896
|
+
const project = requireProject(split.project);
|
|
691
897
|
await cmdCompress(project);
|
|
692
898
|
break;
|
|
693
899
|
}
|
|
@@ -699,10 +905,11 @@ async function main() {
|
|
|
699
905
|
break;
|
|
700
906
|
}
|
|
701
907
|
case "context": {
|
|
702
|
-
const
|
|
703
|
-
|
|
908
|
+
const split = splitProjectFromVariadic(args);
|
|
909
|
+
const project = requireProject(split.project);
|
|
910
|
+
if (split.rest.length === 0)
|
|
704
911
|
usage();
|
|
705
|
-
await cmdContext(project, rest.join(" "));
|
|
912
|
+
await cmdContext(project, split.rest.join(" "));
|
|
706
913
|
break;
|
|
707
914
|
}
|
|
708
915
|
case "help": {
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import nodeFs from "node:fs";
|
|
3
|
+
import nodeOs from "node:os";
|
|
3
4
|
import nodePath from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { createInterface } from "node:readline/promises";
|
|
@@ -24,6 +25,7 @@ const CLI_PATH = fileURLToPath(import.meta.url);
|
|
|
24
25
|
// directly — tsx's esbuild transform fails when invoked from inside a git
|
|
25
26
|
// hook's stripped-down shell environment.
|
|
26
27
|
const DIST_CLI_PATH = nodePath.join(nodePath.dirname(CLI_PATH), "..", "dist", "cli.js");
|
|
28
|
+
const DIST_INDEX_PATH = nodePath.join(nodePath.dirname(CLI_PATH), "..", "dist", "index.js");
|
|
27
29
|
|
|
28
30
|
const MEMORY_TYPES = ["decision", "rejection", "constraint", "discovery"] as const;
|
|
29
31
|
type MemoryType = (typeof MEMORY_TYPES)[number];
|
|
@@ -37,64 +39,69 @@ const TYPE_LABELS: Record<MemoryType, string> = {
|
|
|
37
39
|
|
|
38
40
|
function usage(): never {
|
|
39
41
|
console.error(`Usage:
|
|
40
|
-
cm start
|
|
41
|
-
cm save
|
|
42
|
-
cm fix
|
|
43
|
-
cm analyze
|
|
44
|
-
cm search
|
|
45
|
-
cm resolve
|
|
46
|
-
cm delete
|
|
47
|
-
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]
|
|
48
50
|
cm init <project> <path>
|
|
49
|
-
cm context
|
|
50
|
-
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.`);
|
|
51
56
|
process.exit(1);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
function cmdHelp() {
|
|
55
60
|
console.log(`cm — coding memory CLI
|
|
56
61
|
|
|
57
|
-
|
|
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]
|
|
58
66
|
Load all unresolved memories for a project, sorted by decay
|
|
59
67
|
score and capped at a 2000 token context budget, grouped by
|
|
60
68
|
type (decisions, constraints, discoveries, rejections), with
|
|
61
69
|
linked memory ids shown under each entry.
|
|
62
70
|
|
|
63
|
-
cm save
|
|
71
|
+
cm save [project] <type> <content> [--force]
|
|
64
72
|
Save a new memory. <type> is one of: decision, rejection,
|
|
65
73
|
constraint, discovery. Automatically links to related existing
|
|
66
74
|
memories. If the new memory contradicts an existing one, prompts
|
|
67
75
|
to keep the old memory or replace it — pass --force to skip the
|
|
68
76
|
prompt and always replace.
|
|
69
77
|
|
|
70
|
-
cm fix
|
|
78
|
+
cm fix [project] <problem> <solution>
|
|
71
79
|
Record a problem and its solution for future reference.
|
|
72
80
|
|
|
73
|
-
cm analyze
|
|
81
|
+
cm analyze [project] <path>
|
|
74
82
|
Build an AST index (files, functions, classes) for the codebase
|
|
75
83
|
at <path>.
|
|
76
84
|
|
|
77
|
-
cm search
|
|
85
|
+
cm search [project] <query>
|
|
78
86
|
Search saved memories and past fixes for a project matching
|
|
79
87
|
<query>.
|
|
80
88
|
|
|
81
|
-
cm resolve
|
|
89
|
+
cm resolve [project] <memory-id>
|
|
82
90
|
Mark a memory as resolved.
|
|
83
91
|
|
|
84
|
-
cm delete
|
|
92
|
+
cm delete [project] <memory-id>
|
|
85
93
|
Delete a memory and any memory_links referencing it.
|
|
86
94
|
|
|
87
|
-
cm compress
|
|
95
|
+
cm compress [project]
|
|
88
96
|
Cluster related unresolved memories (needs 20+) and collapse
|
|
89
97
|
clusters of 3 or more into a single summary memory.
|
|
90
98
|
|
|
91
99
|
cm init <project> <path>
|
|
92
|
-
Install a git post-commit hook in the repo at <path
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
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.
|
|
96
103
|
|
|
97
|
-
cm context
|
|
104
|
+
cm context [project] <task>
|
|
98
105
|
Return only the top 5 memories most relevant to a specific
|
|
99
106
|
task, ranked by a blend of task relevance and decay score,
|
|
100
107
|
as a markdown block capped at a 2000 token budget.
|
|
@@ -108,6 +115,53 @@ function fail(message: string): never {
|
|
|
108
115
|
process.exit(1);
|
|
109
116
|
}
|
|
110
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
|
+
|
|
111
165
|
// --- cm start ----------------------------------------------------------
|
|
112
166
|
|
|
113
167
|
async function cmdStart(project: string) {
|
|
@@ -469,11 +523,20 @@ async function cmdDelete(project: string, memoryId: string) {
|
|
|
469
523
|
// .git/hooks/post-commit) so its module type is unambiguous — an
|
|
470
524
|
// extensionless file run by git would otherwise have its CommonJS/ESM
|
|
471
525
|
// interpretation depend on the target repo's own package.json.
|
|
472
|
-
function buildHookLogicScript(
|
|
526
|
+
function buildHookLogicScript(cliPath: string): string {
|
|
473
527
|
return `#!/usr/bin/env node
|
|
474
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
|
+
}
|
|
475
539
|
|
|
476
|
-
const PROJECT = ${JSON.stringify(project)};
|
|
477
540
|
const CLI_PATH = ${JSON.stringify(cliPath)};
|
|
478
541
|
|
|
479
542
|
function git(args) {
|
|
@@ -648,6 +711,128 @@ for (const item of savedItems) {
|
|
|
648
711
|
`;
|
|
649
712
|
}
|
|
650
713
|
|
|
714
|
+
function registerMcpServer(distIndexPath: string): void {
|
|
715
|
+
const claudeConfigPath = nodePath.join(nodeOs.homedir(), ".claude.json");
|
|
716
|
+
|
|
717
|
+
let config: { mcpServers?: Record<string, unknown> } = {};
|
|
718
|
+
if (nodeFs.existsSync(claudeConfigPath)) {
|
|
719
|
+
try {
|
|
720
|
+
config = JSON.parse(nodeFs.readFileSync(claudeConfigPath, "utf-8"));
|
|
721
|
+
} catch {
|
|
722
|
+
config = {};
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
727
|
+
config.mcpServers = {};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (config.mcpServers.stackmem) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
config.mcpServers.stackmem = {
|
|
735
|
+
command: "node",
|
|
736
|
+
args: [distIndexPath],
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
nodeFs.writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), "utf-8");
|
|
740
|
+
console.log("stackmem MCP server registered with Claude Code.");
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function writeClaudeMd(project: string, resolvedPath: string): void {
|
|
744
|
+
const claudeMdPath = nodePath.join(resolvedPath, "CLAUDE.md");
|
|
745
|
+
|
|
746
|
+
if (nodeFs.existsSync(claudeMdPath)) {
|
|
747
|
+
console.log("CLAUDE.md already exists — skipping.");
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const content = `# stackmem
|
|
752
|
+
|
|
753
|
+
At the start of every session, the stackmem MCP server will automatically load memories for this project. The project name is "${project}".
|
|
754
|
+
|
|
755
|
+
If the MCP server is not connected, run:
|
|
756
|
+
cm start ${project}
|
|
757
|
+
and paste the output here before starting work.
|
|
758
|
+
|
|
759
|
+
When you make a decision, discover a constraint, reject an approach, or make a discovery during this session, save it with:
|
|
760
|
+
cm save ${project} <type> "<content>"
|
|
761
|
+
`;
|
|
762
|
+
|
|
763
|
+
nodeFs.writeFileSync(claudeMdPath, content, "utf-8");
|
|
764
|
+
console.log("CLAUDE.md written.");
|
|
765
|
+
}
|
|
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
|
+
|
|
651
836
|
async function cmdInit(project: string, targetPath: string) {
|
|
652
837
|
const resolvedPath = nodePath.resolve(targetPath);
|
|
653
838
|
const gitDir = nodePath.join(resolvedPath, ".git");
|
|
@@ -661,7 +846,7 @@ async function cmdInit(project: string, targetPath: string) {
|
|
|
661
846
|
nodeFs.mkdirSync(codingMemoryDir, { recursive: true });
|
|
662
847
|
|
|
663
848
|
const hookLogicPath = nodePath.join(codingMemoryDir, "post-commit-hook.mjs");
|
|
664
|
-
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(
|
|
849
|
+
nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(DIST_CLI_PATH), "utf-8");
|
|
665
850
|
|
|
666
851
|
const hooksDir = nodePath.join(gitDir, "hooks");
|
|
667
852
|
nodeFs.mkdirSync(hooksDir, { recursive: true });
|
|
@@ -671,6 +856,34 @@ async function cmdInit(project: string, targetPath: string) {
|
|
|
671
856
|
nodeFs.chmodSync(hookPath, 0o755);
|
|
672
857
|
|
|
673
858
|
console.log(`coding-memory hook installed for project ${project} at ${targetPath}`);
|
|
859
|
+
|
|
860
|
+
registerMcpServer(DIST_INDEX_PATH);
|
|
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);
|
|
674
887
|
}
|
|
675
888
|
|
|
676
889
|
// --- cm context ------------------------------------------------------------
|
|
@@ -736,8 +949,9 @@ async function main() {
|
|
|
736
949
|
|
|
737
950
|
switch (command) {
|
|
738
951
|
case "start": {
|
|
739
|
-
const
|
|
740
|
-
if (!
|
|
952
|
+
const split = splitOptionalProject(args, 0);
|
|
953
|
+
if (!split) usage();
|
|
954
|
+
const project = requireProject(split.project);
|
|
741
955
|
await cmdStart(project);
|
|
742
956
|
break;
|
|
743
957
|
}
|
|
@@ -747,44 +961,66 @@ async function main() {
|
|
|
747
961
|
const positional = force
|
|
748
962
|
? [...args.slice(0, forceIndex), ...args.slice(forceIndex + 1)]
|
|
749
963
|
: args;
|
|
750
|
-
|
|
751
|
-
|
|
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);
|
|
752
978
|
await cmdSave(project, type, rest.join(" "), force);
|
|
753
979
|
break;
|
|
754
980
|
}
|
|
755
981
|
case "fix": {
|
|
756
|
-
const
|
|
757
|
-
if (!
|
|
982
|
+
const split = splitOptionalProject(args, 2);
|
|
983
|
+
if (!split) usage();
|
|
984
|
+
const project = requireProject(split.project);
|
|
985
|
+
const [problem, solution] = split.rest;
|
|
758
986
|
await cmdFix(project, problem, solution);
|
|
759
987
|
break;
|
|
760
988
|
}
|
|
761
989
|
case "analyze": {
|
|
762
|
-
const
|
|
763
|
-
if (!
|
|
990
|
+
const split = splitOptionalProject(args, 1);
|
|
991
|
+
if (!split) usage();
|
|
992
|
+
const project = requireProject(split.project);
|
|
993
|
+
const [path] = split.rest;
|
|
764
994
|
await cmdAnalyze(project, path);
|
|
765
995
|
break;
|
|
766
996
|
}
|
|
767
997
|
case "search": {
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
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(" "));
|
|
771
1002
|
break;
|
|
772
1003
|
}
|
|
773
1004
|
case "resolve": {
|
|
774
|
-
const
|
|
775
|
-
if (!
|
|
1005
|
+
const split = splitOptionalProject(args, 1);
|
|
1006
|
+
if (!split) usage();
|
|
1007
|
+
const project = requireProject(split.project);
|
|
1008
|
+
const [memoryId] = split.rest;
|
|
776
1009
|
await cmdResolve(project, memoryId);
|
|
777
1010
|
break;
|
|
778
1011
|
}
|
|
779
1012
|
case "delete": {
|
|
780
|
-
const
|
|
781
|
-
if (!
|
|
1013
|
+
const split = splitOptionalProject(args, 1);
|
|
1014
|
+
if (!split) usage();
|
|
1015
|
+
const project = requireProject(split.project);
|
|
1016
|
+
const [memoryId] = split.rest;
|
|
782
1017
|
await cmdDelete(project, memoryId);
|
|
783
1018
|
break;
|
|
784
1019
|
}
|
|
785
1020
|
case "compress": {
|
|
786
|
-
const
|
|
787
|
-
if (!
|
|
1021
|
+
const split = splitOptionalProject(args, 0);
|
|
1022
|
+
if (!split) usage();
|
|
1023
|
+
const project = requireProject(split.project);
|
|
788
1024
|
await cmdCompress(project);
|
|
789
1025
|
break;
|
|
790
1026
|
}
|
|
@@ -795,9 +1031,10 @@ async function main() {
|
|
|
795
1031
|
break;
|
|
796
1032
|
}
|
|
797
1033
|
case "context": {
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
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(" "));
|
|
801
1038
|
break;
|
|
802
1039
|
}
|
|
803
1040
|
case "help": {
|