stackmem 1.0.0

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 ADDED
@@ -0,0 +1,223 @@
1
+ # coding-memory
2
+
3
+ AI agents forget everything between sessions. Every new chat re-derives decisions, re-breaks constraints that were already settled, and re-debugs errors that were already fixed last week.
4
+
5
+ coding-memory is a CLI and an MCP server that stores that context in Supabase and hands it back at the start of the next session. A git post-commit hook captures most of it automatically.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ git clone https://github.com/arushlagudu/coding-memory.git
11
+ cd coding-memory
12
+ npm install
13
+ ```
14
+
15
+ Create `.env`:
16
+
17
+ ```
18
+ SUPABASE_URL=https://your-project.supabase.co
19
+ SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
20
+ ```
21
+
22
+ Build and link:
23
+
24
+ ```
25
+ npm run build
26
+ sudo npm link
27
+ sudo npm install -g tsx
28
+ ```
29
+
30
+ `cm` is now on your PATH. Install the hook in a project:
31
+
32
+ ```
33
+ $ cd your-project-directory
34
+ $ cm init myapp .
35
+ coding-memory hook installed for project myapp at .
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```
41
+ cm start <project>
42
+ cm save <project> <type> <content> [--force]
43
+ cm fix <project> <problem> <solution>
44
+ cm analyze <project> <path>
45
+ cm search <project> <query>
46
+ cm resolve <project> <memory-id>
47
+ cm delete <project> <memory-id>
48
+ cm context <project> <task>
49
+ cm compress <project>
50
+ cm init <project> <path>
51
+ cm help
52
+ ```
53
+
54
+ ### cm start
55
+
56
+ ```
57
+ $ cm start myapp
58
+ Loaded 2 memory(ies) for "myapp".
59
+
60
+ Decisions (1)
61
+ -------------
62
+ [294fba3a-aef3-472a-933f-b6bfd41c7cfa] Use Supabase for persistence layer
63
+ linked: 793a0053-83b6-463a-9e6d-df8b7c2fbed8
64
+
65
+ Constraints (1)
66
+ ---------------
67
+ [793a0053-83b6-463a-9e6d-df8b7c2fbed8] Service role key must never be exposed to a client
68
+ linked: 294fba3a-aef3-472a-933f-b6bfd41c7cfa
69
+ ```
70
+
71
+ ### cm save
72
+
73
+ `type` is one of `decision`, `rejection`, `constraint`, `discovery`.
74
+
75
+ ```
76
+ $ cm save myapp decision "Use ilike search across memories and execution_log"
77
+ Saved decision [294fba3a-aef3-472a-933f-b6bfd41c7cfa] for "myapp".
78
+ ```
79
+
80
+ If the new content contradicts something already stored, it asks first.
81
+
82
+ ```
83
+ $ cm save myapp decision "We switched from Supabase to Postgres directly"
84
+ Conflict detected:
85
+ Old: Use Supabase for persistence layer
86
+ New: We switched from Supabase to Postgres directly
87
+
88
+ Keep old or replace? (k/r): r
89
+ Replaced with new memory [b1a2c3d4-5678-90ab-cdef-1234567890ab].
90
+ ```
91
+
92
+ `--force` skips the prompt and always replaces. Use it from scripts.
93
+
94
+ ```
95
+ $ cm save myapp decision "We switched from Supabase to Postgres directly" --force
96
+ Replaced with new memory [b1a2c3d4-5678-90ab-cdef-1234567890ab].
97
+ Saved decision [b1a2c3d4-5678-90ab-cdef-1234567890ab] for "myapp".
98
+ ```
99
+
100
+ ### cm fix
101
+
102
+ ```
103
+ $ cm fix myapp "tsc failed with Node16 module resolution error" "switched moduleResolution to Node16"
104
+ Fix recorded for "myapp".
105
+ ```
106
+
107
+ ### cm analyze
108
+
109
+ ```
110
+ $ cm analyze myapp .
111
+ Indexed 7 file(s):
112
+ functions: 24
113
+ classes: 0
114
+ ```
115
+
116
+ Runs tree-sitter over the project and writes an AST index to `.coding-memory/ast-index.db`.
117
+
118
+ ### cm search
119
+
120
+ Checks memories and past fixes in the same query.
121
+
122
+ ```
123
+ $ cm search myapp "module resolution"
124
+ Memories (0)
125
+ No results found
126
+
127
+ Past Fixes (1)
128
+ Problem: tsc failed with Node16 module resolution error
129
+ Solution: switched moduleResolution to Node16
130
+ ```
131
+
132
+ ### cm resolve / cm delete
133
+
134
+ ```
135
+ $ cm resolve myapp 294fba3a-aef3-472a-933f-b6bfd41c7cfa
136
+ Resolved memory [294fba3a-aef3-472a-933f-b6bfd41c7cfa]
137
+
138
+ $ cm delete myapp 793a0053-83b6-463a-9e6d-df8b7c2fbed8
139
+ Deleted memory [793a0053-83b6-463a-9e6d-df8b7c2fbed8]
140
+ ```
141
+
142
+ `delete` also removes any memory_links pointing at that id.
143
+
144
+ ### cm context
145
+
146
+ ```
147
+ $ cm context myapp "fix the module resolution error in the build"
148
+ ## Project Context: myapp
149
+ ### Task: fix the module resolution error in the build
150
+
151
+ **Constraints**
152
+ - Service role key must never be exposed to a client
153
+
154
+ **Discoveries**
155
+ - schema change: migrations/003_add_access_count.sql
156
+
157
+ *(41/2000 tokens used)*
158
+ ```
159
+
160
+ Top 5 memories relevant to a task, ranked by relevance and decay, capped at 2000 tokens. Feed this to an agent instead of the whole project's memory.
161
+
162
+ ### cm compress
163
+
164
+ ```
165
+ $ cm compress myapp
166
+ Compressed 4 memories into 1 summary
167
+ Compressed 3 memories into 1 summary
168
+ Compression complete. 2 clusters compressed.
169
+ ```
170
+
171
+ Needs 20+ unresolved memories. Also runs automatically once a project crosses that count.
172
+
173
+ ### cm init
174
+
175
+ ```
176
+ $ cm init myapp .
177
+ coding-memory hook installed for project myapp at .
178
+ ```
179
+
180
+ Installs a post-commit hook that calls `dist/cli.js save` after every commit with the commit message, plus whatever the diff turns up: removed imports, new env vars, new auth files, package.json changes, new config files, schema files, TODOs.
181
+
182
+ ### cm help
183
+
184
+ ```
185
+ $ cm help
186
+ cm — coding memory CLI
187
+
188
+ cm start <project>
189
+ Load all unresolved memories for a project, sorted by decay
190
+ score and capped at a 2000 token context budget.
191
+
192
+ cm save <project> <type> <content> [--force]
193
+ Save a new memory. Prompts before overwriting a contradiction
194
+ unless --force is passed.
195
+
196
+ ...
197
+ ```
198
+
199
+ ## How it works
200
+
201
+ Memories are decisions, constraints, discoveries, and rejections, scoped to a project. Fixes are stored separately as a problem string and the solution that closed it, so the same tsc error doesn't cost another 20 minutes next month.
202
+
203
+ Every save checks new content against existing memories with a containment score: shared terms over the size of the smaller set. Above 0.85 with no negation word, it's a duplicate and gets skipped. With a negation word like "switched" or "no longer" and a score above 0.4, it's a contradiction.
204
+
205
+ cm save stops and asks whether to keep the old memory or replace it, unless `--force` is passed. The MCP `save_memory` tool always replaces automatically, since nothing is watching stdin on that path.
206
+
207
+ New memories are also compared against everything else and linked above a lower threshold of 0.15. `cm start` and `cm context` show those links without any manual tagging.
208
+
209
+ Relevance decays over time: `score = (1 + ln(1 + accessCount)) * exp(-0.05 * daysSinceCreated)`. Frequently accessed memories decay slower, but nothing survives forever. The 0.05 constant works out to roughly a 14-day half life.
210
+
211
+ `cm start` and `cm context` cap output at 2000 tokens. They fill from the highest-ranked memory down until the next one would push past that limit.
212
+
213
+ Past 20 unresolved memories, `cm compress` clusters related memories by containment score above 0.2 using union-find. Clusters of 3 or more collapse into a single summary memory.
214
+
215
+ ## Research
216
+
217
+ Ideas taken from a few papers, not implementations of them.
218
+
219
+ - Codebase-Memory: Tree-Sitter-Based Knowledge Graphs for LLM Code Exploration via MCP (arXiv:2603.27277)
220
+ - Feedback-Normalized Developer Memory for Reinforcement-Learning Coding Agents (arXiv:2605.01567)
221
+ - A-MEM: Agentic Memory for LLM Agents (arXiv:2502.12110, NeurIPS 2025)
222
+ - SWE-MeM: Learning Adaptive Memory Management for Long-Horizon Coding Agents (arXiv:2606.28434)
223
+ - AtomMem: Learnable Dynamic Agentic Memory with Atomic Memory Operation (arXiv:2601.08323)
@@ -0,0 +1,272 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import Parser from "tree-sitter";
5
+ import TreeSitterTypeScript from "tree-sitter-typescript";
6
+ import TreeSitterJavaScript from "tree-sitter-javascript";
7
+ const TypeScript = TreeSitterTypeScript.typescript;
8
+ const SKIP_DIRS = new Set([
9
+ "node_modules",
10
+ ".git",
11
+ "dist",
12
+ "build",
13
+ "out",
14
+ ".next",
15
+ ".turbo",
16
+ "coverage",
17
+ ".coding-memory",
18
+ ]);
19
+ const FUNCTION_NODE_TYPES = new Set([
20
+ "function_declaration",
21
+ "function_expression",
22
+ "generator_function_declaration",
23
+ "generator_function",
24
+ "method_definition",
25
+ ]);
26
+ function getDbPath(projectPath) {
27
+ return path.join(projectPath, ".coding-memory", "ast-index.db");
28
+ }
29
+ function openDb(projectPath) {
30
+ const dbDir = path.join(projectPath, ".coding-memory");
31
+ fs.mkdirSync(dbDir, { recursive: true });
32
+ const db = new Database(getDbPath(projectPath));
33
+ db.pragma("journal_mode = WAL");
34
+ db.exec(`
35
+ CREATE TABLE IF NOT EXISTS files (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ file_path TEXT NOT NULL UNIQUE,
38
+ file_name TEXT NOT NULL,
39
+ indexed_at TEXT NOT NULL
40
+ );
41
+ CREATE TABLE IF NOT EXISTS functions (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
44
+ name TEXT NOT NULL
45
+ );
46
+ CREATE TABLE IF NOT EXISTS classes (
47
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
49
+ name TEXT NOT NULL
50
+ );
51
+ CREATE TABLE IF NOT EXISTS imports (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
54
+ source TEXT NOT NULL
55
+ );
56
+ `);
57
+ return db;
58
+ }
59
+ function walkProjectFiles(projectPath) {
60
+ const results = [];
61
+ function walk(dir) {
62
+ let entries;
63
+ try {
64
+ entries = fs.readdirSync(dir, { withFileTypes: true });
65
+ }
66
+ catch {
67
+ return;
68
+ }
69
+ for (const entry of entries) {
70
+ if (entry.isDirectory()) {
71
+ if (SKIP_DIRS.has(entry.name))
72
+ continue;
73
+ walk(path.join(dir, entry.name));
74
+ }
75
+ else if (entry.isFile()) {
76
+ if (/\.(ts|js)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
77
+ results.push(path.join(dir, entry.name));
78
+ }
79
+ }
80
+ }
81
+ }
82
+ walk(projectPath);
83
+ return results;
84
+ }
85
+ function nameOfNode(node) {
86
+ if (!node)
87
+ return null;
88
+ return node.text;
89
+ }
90
+ function extractIdentifierFromLhs(node) {
91
+ // For `const foo = () => {}` / `const foo = function () {}`, the
92
+ // enclosing variable_declarator's `name` field holds the identifier.
93
+ if (node.type === "identifier")
94
+ return node.text;
95
+ return null;
96
+ }
97
+ function extractFromSource(sourceCode, language) {
98
+ const parser = new Parser();
99
+ parser.setLanguage(language);
100
+ const tree = parser.parse(sourceCode);
101
+ const functions = [];
102
+ const classes = [];
103
+ const imports = [];
104
+ function visit(node) {
105
+ switch (node.type) {
106
+ case "function_declaration":
107
+ case "generator_function_declaration": {
108
+ const name = nameOfNode(node.childForFieldName("name"));
109
+ if (name)
110
+ functions.push(name);
111
+ break;
112
+ }
113
+ case "method_definition": {
114
+ const name = nameOfNode(node.childForFieldName("name"));
115
+ if (name)
116
+ functions.push(name);
117
+ break;
118
+ }
119
+ case "function_expression":
120
+ case "generator_function":
121
+ case "arrow_function": {
122
+ const declaredName = nameOfNode(node.childForFieldName("name"));
123
+ if (declaredName) {
124
+ functions.push(declaredName);
125
+ }
126
+ else if (node.parent?.type === "variable_declarator") {
127
+ const lhs = extractIdentifierFromLhs(node.parent.childForFieldName("name"));
128
+ if (lhs)
129
+ functions.push(lhs);
130
+ }
131
+ break;
132
+ }
133
+ case "class_declaration":
134
+ case "class": {
135
+ const name = nameOfNode(node.childForFieldName("name"));
136
+ if (name) {
137
+ classes.push(name);
138
+ }
139
+ else if (node.parent?.type === "variable_declarator") {
140
+ const lhs = extractIdentifierFromLhs(node.parent.childForFieldName("name"));
141
+ if (lhs)
142
+ classes.push(lhs);
143
+ }
144
+ break;
145
+ }
146
+ case "import_statement": {
147
+ const source = node.childForFieldName("source");
148
+ if (source)
149
+ imports.push(source.text.replace(/^['"]|['"]$/g, ""));
150
+ break;
151
+ }
152
+ case "call_expression": {
153
+ const fn = node.childForFieldName("function");
154
+ if (fn?.type === "identifier" && fn.text === "require") {
155
+ const args = node.childForFieldName("arguments");
156
+ const firstArg = args?.namedChild(0);
157
+ if (firstArg?.type === "string") {
158
+ imports.push(firstArg.text.replace(/^['"]|['"]$/g, ""));
159
+ }
160
+ }
161
+ break;
162
+ }
163
+ }
164
+ for (const child of node.namedChildren) {
165
+ visit(child);
166
+ }
167
+ }
168
+ visit(tree.rootNode);
169
+ return { functions, classes, imports };
170
+ }
171
+ function extractFile(filePath, projectPath) {
172
+ const sourceCode = fs.readFileSync(filePath, "utf-8");
173
+ const language = filePath.endsWith(".ts") ? TypeScript : TreeSitterJavaScript;
174
+ const { functions, classes, imports } = extractFromSource(sourceCode, language);
175
+ return {
176
+ relativePath: path.relative(projectPath, filePath),
177
+ functions,
178
+ classes,
179
+ imports,
180
+ };
181
+ }
182
+ export function analyzeCodebase(projectPath) {
183
+ const resolvedProjectPath = path.resolve(projectPath);
184
+ const db = openDb(resolvedProjectPath);
185
+ const insertFile = db.prepare("INSERT INTO files (file_path, file_name, indexed_at) VALUES (?, ?, ?) ON CONFLICT(file_path) DO UPDATE SET indexed_at = excluded.indexed_at RETURNING id");
186
+ const deleteChildren = db.prepare("DELETE FROM functions WHERE file_id = ?");
187
+ const deleteClasses = db.prepare("DELETE FROM classes WHERE file_id = ?");
188
+ const deleteImports = db.prepare("DELETE FROM imports WHERE file_id = ?");
189
+ const insertFunction = db.prepare("INSERT INTO functions (file_id, name) VALUES (?, ?)");
190
+ const insertClass = db.prepare("INSERT INTO classes (file_id, name) VALUES (?, ?)");
191
+ const insertImport = db.prepare("INSERT INTO imports (file_id, source) VALUES (?, ?)");
192
+ const files = walkProjectFiles(resolvedProjectPath);
193
+ let totalFunctions = 0;
194
+ let totalClasses = 0;
195
+ const runAll = db.transaction((filePaths) => {
196
+ for (const filePath of filePaths) {
197
+ const extraction = extractFile(filePath, resolvedProjectPath);
198
+ const row = insertFile.get(extraction.relativePath, path.basename(extraction.relativePath), new Date().toISOString());
199
+ const fileId = row.id;
200
+ deleteChildren.run(fileId);
201
+ deleteClasses.run(fileId);
202
+ deleteImports.run(fileId);
203
+ for (const name of extraction.functions) {
204
+ insertFunction.run(fileId, name);
205
+ totalFunctions++;
206
+ }
207
+ for (const name of extraction.classes) {
208
+ insertClass.run(fileId, name);
209
+ totalClasses++;
210
+ }
211
+ for (const source of extraction.imports) {
212
+ insertImport.run(fileId, source);
213
+ }
214
+ }
215
+ });
216
+ runAll(files);
217
+ db.close();
218
+ return {
219
+ files_indexed: files.length,
220
+ functions: totalFunctions,
221
+ classes: totalClasses,
222
+ };
223
+ }
224
+ function findFileRow(db, fileName) {
225
+ const exact = db
226
+ .prepare("SELECT id, file_path, file_name FROM files WHERE file_path = ?")
227
+ .get(fileName);
228
+ if (exact)
229
+ return exact;
230
+ return db
231
+ .prepare("SELECT id, file_path, file_name FROM files WHERE file_name = ?")
232
+ .get(path.basename(fileName));
233
+ }
234
+ export function getFileSummary(projectPath, fileName) {
235
+ const resolvedProjectPath = path.resolve(projectPath);
236
+ const dbPath = getDbPath(resolvedProjectPath);
237
+ if (!fs.existsSync(dbPath)) {
238
+ throw new Error(`No AST index found at ${dbPath}. Run analyze_codebase on this project first.`);
239
+ }
240
+ const db = new Database(dbPath, { readonly: true });
241
+ try {
242
+ const fileRow = findFileRow(db, fileName);
243
+ if (!fileRow) {
244
+ throw new Error(`File "${fileName}" not found in the AST index for ${resolvedProjectPath}.`);
245
+ }
246
+ const functions = db.prepare("SELECT name FROM functions WHERE file_id = ?").all(fileRow.id).map((r) => r.name);
247
+ const classes = db.prepare("SELECT name FROM classes WHERE file_id = ?").all(fileRow.id).map((r) => r.name);
248
+ return { file_path: fileRow.file_path, functions, classes };
249
+ }
250
+ finally {
251
+ db.close();
252
+ }
253
+ }
254
+ export function findDependencies(projectPath, fileName) {
255
+ const resolvedProjectPath = path.resolve(projectPath);
256
+ const dbPath = getDbPath(resolvedProjectPath);
257
+ if (!fs.existsSync(dbPath)) {
258
+ throw new Error(`No AST index found at ${dbPath}. Run analyze_codebase on this project first.`);
259
+ }
260
+ const db = new Database(dbPath, { readonly: true });
261
+ try {
262
+ const fileRow = findFileRow(db, fileName);
263
+ if (!fileRow) {
264
+ throw new Error(`File "${fileName}" not found in the AST index for ${resolvedProjectPath}.`);
265
+ }
266
+ const imports = db.prepare("SELECT source FROM imports WHERE file_id = ?").all(fileRow.id).map((r) => r.source);
267
+ return { file_path: fileRow.file_path, imports };
268
+ }
269
+ finally {
270
+ db.close();
271
+ }
272
+ }