stackmem 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ast-index.js CHANGED
@@ -190,11 +190,22 @@ export function analyzeCodebase(projectPath) {
190
190
  const insertClass = db.prepare("INSERT INTO classes (file_id, name) VALUES (?, ?)");
191
191
  const insertImport = db.prepare("INSERT INTO imports (file_id, source) VALUES (?, ?)");
192
192
  const files = walkProjectFiles(resolvedProjectPath);
193
+ let filesIndexed = 0;
193
194
  let totalFunctions = 0;
194
195
  let totalClasses = 0;
195
196
  const runAll = db.transaction((filePaths) => {
196
197
  for (const filePath of filePaths) {
197
- const extraction = extractFile(filePath, resolvedProjectPath);
198
+ let extraction;
199
+ try {
200
+ extraction = extractFile(filePath, resolvedProjectPath);
201
+ }
202
+ catch {
203
+ // Tree-sitter (or the file read) choked on this file — better-sqlite3
204
+ // transactions roll back entirely on a throw, so skipping here
205
+ // (rather than letting it propagate) is what keeps one bad file from
206
+ // wiping out every other file already processed in this run.
207
+ continue;
208
+ }
198
209
  const row = insertFile.get(extraction.relativePath, path.basename(extraction.relativePath), new Date().toISOString());
199
210
  const fileId = row.id;
200
211
  deleteChildren.run(fileId);
@@ -211,12 +222,13 @@ export function analyzeCodebase(projectPath) {
211
222
  for (const source of extraction.imports) {
212
223
  insertImport.run(fileId, source);
213
224
  }
225
+ filesIndexed++;
214
226
  }
215
227
  });
216
228
  runAll(files);
217
229
  db.close();
218
230
  return {
219
- files_indexed: files.length,
231
+ files_indexed: filesIndexed,
220
232
  functions: totalFunctions,
221
233
  classes: totalClasses,
222
234
  };
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { analyzeCodebase } from "./ast-index.js";
8
8
  import { getDeviceId } from "./device.js";
9
- import { applyContextBudget, buildSummaryContent, clusterMemories, computeDecayScore, computeMemoryLinks, decideSaveAction, estimateTokens, scoreMemoryRelevance, } from "./scoring.js";
9
+ import { applyContextBudget, buildSummaryContent, clusterMemories, computeDecayScore, computeMemoryLinks, decideSaveAction, estimateTokens, indicatesFailedApproach, isAuthRelatedFile, isCodeFile, isQualityMessage, scoreMemoryRelevance, } from "./scoring.js";
10
10
  import { supabase } from "./storage.js";
11
11
  const CONTEXT_BUDGET_TOKENS = 2000;
12
12
  const COMPRESSION_THRESHOLD = 20;
@@ -29,16 +29,23 @@ function usage() {
29
29
  cm save [project] <type> <content> [--force] (type: decision|rejection|constraint|discovery)
30
30
  cm fix [project] <problem> <solution>
31
31
  cm analyze [project] <path>
32
- cm search [project] <query>
32
+ cm search [--project <name>] <query>
33
33
  cm resolve [project] <memory-id>
34
34
  cm delete [project] <memory-id>
35
35
  cm compress [project]
36
36
  cm init <project> <path>
37
- cm context [project] <task>
37
+ cm context [--project <name>] <task>
38
+ cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
39
+ cm doctor
40
+ cm undo [project]
41
+ cm export [filename]
42
+ cm import [project] <filename>
38
43
  cm help
39
44
 
40
45
  [project] is optional if a .stackmem file exists in the current
41
- directory (written by 'cm init'). Otherwise it must be given explicitly.`);
46
+ directory (written by 'cm init'). Otherwise it must be given explicitly.
47
+ search/context take --project instead of a positional project, since a
48
+ bare query/task can't otherwise be told apart from a project name.`);
42
49
  process.exit(1);
43
50
  }
44
51
  function cmdHelp() {
@@ -67,9 +74,10 @@ function cmdHelp() {
67
74
  Build an AST index (files, functions, classes) for the codebase
68
75
  at <path>.
69
76
 
70
- cm search [project] <query>
77
+ cm search [--project <name>] <query>
71
78
  Search saved memories and past fixes for a project matching
72
- <query>.
79
+ <query>. The whole query is always the full remaining text — use
80
+ --project to search a project other than the one in .stackmem.
73
81
 
74
82
  cm resolve [project] <memory-id>
75
83
  Mark a memory as resolved.
@@ -86,10 +94,42 @@ function cmdHelp() {
86
94
  the stackmem MCP server with Claude Code, write a CLAUDE.md and
87
95
  .stackmem file, and seed initial memories from the project scan.
88
96
 
89
- cm context [project] <task>
97
+ cm context [--project <name>] <task>
90
98
  Return only the top 5 memories most relevant to a specific
91
99
  task, ranked by a blend of task relevance and decay score,
92
- as a markdown block capped at a 2000 token budget.
100
+ as a markdown block capped at a 2000 token budget. Use
101
+ --project to target a project other than the one in .stackmem.
102
+
103
+ cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
104
+ Capture what happened in a session and extract memories: each
105
+ error becomes an unsolved execution_log entry, each approach
106
+ containing "failed", "didn't work", "reverted", or "switched
107
+ away" becomes a rejection, a summary that passes the quality
108
+ filter becomes a decision, auth-related files become a
109
+ discovery, and any .ts/.js file triggers an AST re-index.
110
+ Accepts the same fields as JSON on stdin instead of flags
111
+ (project, summary, errors_encountered, files_touched,
112
+ approaches_tried) when stdin is piped rather than a terminal.
113
+
114
+ cm doctor
115
+ Check every integration point in the current directory — backend
116
+ connection, MCP registration, .stackmem, git hook, and memory
117
+ count — and report what's working and what's broken.
118
+
119
+ cm undo [project]
120
+ Delete the most recently saved unresolved memory (and any
121
+ memory_links referencing it) for a project.
122
+
123
+ cm export [filename]
124
+ Export all unresolved memories, execution_log entries, and
125
+ memory_links for a project to a JSON file. Defaults to
126
+ <project>-memories.json.
127
+
128
+ cm import [project] <filename>
129
+ Import memories and execution_log entries from a JSON file
130
+ produced by 'cm export'. Inserted under the current device_id
131
+ and project; memory_links are skipped and get rebuilt by
132
+ semantic linking the next time cm save runs.
93
133
 
94
134
  cm help
95
135
  Show this help message.`);
@@ -107,7 +147,18 @@ function readProjectFromFile() {
107
147
  const content = nodeFs.readFileSync(stackmemPath, "utf-8").trim();
108
148
  return content.length > 0 ? content : null;
109
149
  }
150
+ // Catches the common habit-slip of typing `cm fix . "problem" "solution"` by
151
+ // muscle memory from `cm init <project> .` — "." (or "..", or anything
152
+ // path-shaped) is never a real project name, so an explicit arg this shape
153
+ // almost certainly means the caller meant to omit the project entirely.
154
+ function looksLikeAPath(value) {
155
+ return value === "." || value === ".." || value.includes("/") || value.includes("\\");
156
+ }
110
157
  function requireProject(explicit) {
158
+ if (explicit && looksLikeAPath(explicit)) {
159
+ console.error(`Warning: '${explicit}' is not a valid project name — reading from .stackmem instead.`);
160
+ explicit = null;
161
+ }
111
162
  if (explicit)
112
163
  return explicit;
113
164
  const fromFile = readProjectFromFile();
@@ -130,12 +181,23 @@ function splitOptionalProject(args, fixedArgCount) {
130
181
  // arg count can't disambiguate an omitted project from a multi-word query —
131
182
  // "cm search bug fix" is ambiguous by count alone. Use .stackmem's presence
132
183
  // instead: if it exists, nothing is positionally a project.
184
+ // Arg count can't disambiguate an omitted project from a multi-word query —
185
+ // "cm search bug fix" is ambiguous by count alone. Treating .stackmem's mere
186
+ // presence as "project omitted" (the previous approach) was worse: it
187
+ // silently swallowed an explicit project into the query text whenever a
188
+ // .stackmem file happened to exist in cwd, so "cm search otherproj auth bug"
189
+ // would search project X (from .stackmem) for the literal text "otherproj
190
+ // auth bug" with no error. Positional args are now always the full
191
+ // query/task; an explicit override uses --project <name> instead, which is
192
+ // never ambiguous and is never silently absorbed.
133
193
  function splitProjectFromVariadic(args) {
134
- if (readProjectFromFile() !== null) {
135
- return { project: null, rest: args };
194
+ const flagIndex = args.indexOf("--project");
195
+ if (flagIndex !== -1 && args[flagIndex + 1] !== undefined) {
196
+ const project = args[flagIndex + 1];
197
+ const rest = [...args.slice(0, flagIndex), ...args.slice(flagIndex + 2)];
198
+ return { project, rest };
136
199
  }
137
- const [project = null, ...rest] = args;
138
- return { project, rest };
200
+ return { project: null, rest: args };
139
201
  }
140
202
  // --- cm start ----------------------------------------------------------
141
203
  async function cmdStart(project) {
@@ -488,13 +550,17 @@ const LOW_QUALITY_PREFIXES = [
488
550
  "tweak", "change", "stuff", "done", "commit", "save", "ok", "m", "x",
489
551
  ];
490
552
 
553
+ const SEMVER_RE = /^\\d+\\.\\d+\\.\\d+$/;
554
+
491
555
  // 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".
556
+ // and bare version bumps ("1.0.4") so only messages worth remembering
557
+ // become decisions. Checked against the first word, not a raw prefix
558
+ // match, so a real message like "Migrate auth to JWT" isn't rejected just
559
+ // for starting with the letter "m".
495
560
  function isQualityCommitMessage(message) {
496
561
  const trimmed = message.trim();
497
- if (trimmed.length <= 20) return false;
562
+ if (trimmed.length < 15) return false;
563
+ if (SEMVER_RE.test(trimmed)) return false;
498
564
  if (!trimmed.includes(" ")) return false;
499
565
 
500
566
  const stripped = trimmed.replace(/\\s/g, "");
@@ -817,7 +883,7 @@ async function cmdInit(project, targetPath) {
817
883
  console.log(`stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`);
818
884
  }
819
885
  else {
820
- console.log("✗ Backend connection failed — run 'cm doctor' (once we build it)");
886
+ console.log("✗ Backend connection failed — run 'cm doctor'");
821
887
  }
822
888
  // Written last and deliberately: if a commit (and hence the post-commit
823
889
  // hook) fires anywhere during init, it must still see whatever project
@@ -867,6 +933,349 @@ async function cmdContext(project, task) {
867
933
  lines.push(`*(${tokensUsed}/${CONTEXT_BUDGET_TOKENS} tokens used)*`);
868
934
  console.log(lines.join("\n"));
869
935
  }
936
+ function splitCommaList(value) {
937
+ if (!value)
938
+ return [];
939
+ return value
940
+ .split(",")
941
+ .map((item) => item.trim())
942
+ .filter(Boolean);
943
+ }
944
+ // Flags only — stdin JSON is handled separately in cmdSessionEndEntry, since
945
+ // it carries its own full shape (arrays, not comma-joined strings).
946
+ function parseSessionEndFlags(args) {
947
+ let project = null;
948
+ let summary;
949
+ let errors = [];
950
+ let files = [];
951
+ let approaches = [];
952
+ let i = 0;
953
+ if (args[i] && !args[i].startsWith("--")) {
954
+ project = args[i];
955
+ i++;
956
+ }
957
+ for (; i < args.length; i++) {
958
+ const flag = args[i];
959
+ const value = args[i + 1];
960
+ if (flag === "--summary") {
961
+ summary = value;
962
+ i++;
963
+ }
964
+ else if (flag === "--errors") {
965
+ errors = splitCommaList(value);
966
+ i++;
967
+ }
968
+ else if (flag === "--files") {
969
+ files = splitCommaList(value);
970
+ i++;
971
+ }
972
+ else if (flag === "--approaches") {
973
+ approaches = splitCommaList(value);
974
+ i++;
975
+ }
976
+ }
977
+ return { project, summary, errors, files, approaches };
978
+ }
979
+ async function readStdinJson() {
980
+ const chunks = [];
981
+ for await (const chunk of process.stdin) {
982
+ chunks.push(chunk);
983
+ }
984
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
985
+ if (!raw)
986
+ return null;
987
+ try {
988
+ const parsed = JSON.parse(raw);
989
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
990
+ }
991
+ catch {
992
+ return null;
993
+ }
994
+ }
995
+ async function cmdSessionEnd(project, input) {
996
+ let memoriesSaved = 0;
997
+ let errorsLogged = 0;
998
+ for (const problem of input.errors) {
999
+ const { error } = await supabase.from("execution_log").insert({
1000
+ project,
1001
+ problem,
1002
+ solution: null,
1003
+ resolved: false,
1004
+ device_id: getDeviceId(),
1005
+ });
1006
+ if (!error)
1007
+ errorsLogged++;
1008
+ }
1009
+ for (const approach of input.approaches) {
1010
+ if (!indicatesFailedApproach(approach))
1011
+ continue;
1012
+ const { error } = await supabase.from("memories").insert({
1013
+ project,
1014
+ type: "rejection",
1015
+ content: approach,
1016
+ device_id: getDeviceId(),
1017
+ });
1018
+ if (!error)
1019
+ memoriesSaved++;
1020
+ }
1021
+ if (input.summary && isQualityMessage(input.summary)) {
1022
+ const { error } = await supabase.from("memories").insert({
1023
+ project,
1024
+ type: "decision",
1025
+ content: input.summary.trim(),
1026
+ device_id: getDeviceId(),
1027
+ });
1028
+ if (!error)
1029
+ memoriesSaved++;
1030
+ }
1031
+ const authFiles = input.files.filter(isAuthRelatedFile);
1032
+ if (authFiles.length > 0) {
1033
+ const { error } = await supabase.from("memories").insert({
1034
+ project,
1035
+ type: "discovery",
1036
+ content: `session touched auth files: ${authFiles.join(", ")}`,
1037
+ device_id: getDeviceId(),
1038
+ });
1039
+ if (!error)
1040
+ memoriesSaved++;
1041
+ }
1042
+ if (input.files.some(isCodeFile)) {
1043
+ analyzeCodebase(process.cwd());
1044
+ }
1045
+ console.log(`Session captured. ${memoriesSaved} memories saved.`);
1046
+ if (errorsLogged > 0) {
1047
+ console.log(`${errorsLogged} error(s) logged to execution_log.`);
1048
+ }
1049
+ }
1050
+ // Dispatches to JSON-from-stdin or flag parsing depending on how the
1051
+ // command was invoked — piped input (e.g. from a script) uses JSON; an
1052
+ // interactive terminal with no piped stdin uses flags.
1053
+ async function cmdSessionEndEntry(args) {
1054
+ let input;
1055
+ if (!process.stdin.isTTY) {
1056
+ const json = await readStdinJson();
1057
+ if (json) {
1058
+ input = {
1059
+ project: typeof json.project === "string" ? json.project : null,
1060
+ summary: typeof json.summary === "string" ? json.summary : undefined,
1061
+ errors: Array.isArray(json.errors_encountered) ? json.errors_encountered : [],
1062
+ files: Array.isArray(json.files_touched) ? json.files_touched : [],
1063
+ approaches: Array.isArray(json.approaches_tried) ? json.approaches_tried : [],
1064
+ };
1065
+ }
1066
+ else {
1067
+ input = parseSessionEndFlags(args);
1068
+ }
1069
+ }
1070
+ else {
1071
+ input = parseSessionEndFlags(args);
1072
+ }
1073
+ const project = requireProject(input.project);
1074
+ await cmdSessionEnd(project, {
1075
+ summary: input.summary,
1076
+ errors: input.errors,
1077
+ files: input.files,
1078
+ approaches: input.approaches,
1079
+ });
1080
+ }
1081
+ // --- cm doctor ---------------------------------------------------------
1082
+ async function cmdDoctor() {
1083
+ let issues = 0;
1084
+ const project = readProjectFromFile();
1085
+ // 1. Backend connection
1086
+ const backendOk = await verifyBackendConnection(project ?? "");
1087
+ if (backendOk) {
1088
+ console.log("✓ Backend connection verified");
1089
+ }
1090
+ else {
1091
+ console.log("✗ Backend connection failed — check your network");
1092
+ issues++;
1093
+ }
1094
+ // 2. MCP server registration
1095
+ const claudeConfigPath = nodePath.join(nodeOs.homedir(), ".claude.json");
1096
+ let mcpRegistered = false;
1097
+ if (nodeFs.existsSync(claudeConfigPath)) {
1098
+ try {
1099
+ const config = JSON.parse(nodeFs.readFileSync(claudeConfigPath, "utf-8"));
1100
+ mcpRegistered = Boolean(config?.mcpServers?.stackmem);
1101
+ }
1102
+ catch {
1103
+ mcpRegistered = false;
1104
+ }
1105
+ }
1106
+ if (mcpRegistered) {
1107
+ console.log("✓ MCP server registered with Claude Code");
1108
+ }
1109
+ else {
1110
+ console.log("✗ MCP server not registered — run 'cm init <project> .'");
1111
+ issues++;
1112
+ }
1113
+ // 3. Project file
1114
+ if (project) {
1115
+ console.log(`✓ Project file found: ${project}`);
1116
+ }
1117
+ else {
1118
+ console.log("✗ No .stackmem found — run 'cm init <project> .'");
1119
+ issues++;
1120
+ }
1121
+ // 4. Git hook
1122
+ const gitDir = nodePath.join(process.cwd(), ".git");
1123
+ if (!nodeFs.existsSync(gitDir)) {
1124
+ console.log("✗ No git repo found in current directory");
1125
+ issues++;
1126
+ }
1127
+ else {
1128
+ const hookPath = nodePath.join(gitDir, "hooks", "post-commit");
1129
+ let hookExecutable = false;
1130
+ if (nodeFs.existsSync(hookPath)) {
1131
+ try {
1132
+ nodeFs.accessSync(hookPath, nodeFs.constants.X_OK);
1133
+ hookExecutable = true;
1134
+ }
1135
+ catch {
1136
+ hookExecutable = false;
1137
+ }
1138
+ }
1139
+ if (hookExecutable) {
1140
+ console.log("✓ Git hook installed");
1141
+ }
1142
+ else {
1143
+ console.log("✗ Git hook not installed — run 'cm init <project> .'");
1144
+ issues++;
1145
+ }
1146
+ }
1147
+ // 5. Memory count
1148
+ if (project) {
1149
+ const { count, error } = await supabase
1150
+ .from("memories")
1151
+ .select("id", { count: "exact", head: true })
1152
+ .eq("project", project)
1153
+ .eq("resolved", false);
1154
+ if (!error && (count ?? 0) > 0) {
1155
+ console.log(`✓ ${count} memories stored for "${project}"`);
1156
+ }
1157
+ else {
1158
+ console.log(`✗ No memories yet — make a commit to start capturing`);
1159
+ issues++;
1160
+ }
1161
+ }
1162
+ else {
1163
+ console.log(`✗ No memories yet — make a commit to start capturing`);
1164
+ issues++;
1165
+ }
1166
+ console.log("");
1167
+ if (issues === 0) {
1168
+ console.log("All systems operational.");
1169
+ }
1170
+ else {
1171
+ console.log(`${issues} issue(s) found. See above.`);
1172
+ }
1173
+ }
1174
+ // --- cm undo -------------------------------------------------------------
1175
+ async function cmdUndo(project) {
1176
+ const { data, error } = await supabase
1177
+ .from("memories")
1178
+ .select("*")
1179
+ .eq("project", project)
1180
+ .eq("resolved", false)
1181
+ .order("created_at", { ascending: false })
1182
+ .limit(1);
1183
+ if (error)
1184
+ fail(`Failed to load most recent memory: ${error.message}`);
1185
+ const memory = (data ?? [])[0];
1186
+ if (!memory) {
1187
+ console.log("Nothing to undo.");
1188
+ return;
1189
+ }
1190
+ const { error: linksError } = await supabase
1191
+ .from("memory_links")
1192
+ .delete()
1193
+ .or(`source_id.eq.${memory.id},target_id.eq.${memory.id}`);
1194
+ if (linksError)
1195
+ fail(`Failed to delete memory links: ${linksError.message}`);
1196
+ const { error: deleteError } = await supabase.from("memories").delete().eq("id", memory.id);
1197
+ if (deleteError)
1198
+ fail(`Failed to delete memory: ${deleteError.message}`);
1199
+ console.log(`Undone: [${memory.type}] ${memory.content}`);
1200
+ }
1201
+ // --- cm export -------------------------------------------------------------
1202
+ async function cmdExport(project, filename) {
1203
+ const { data: memories, error: memoriesError } = await supabase
1204
+ .from("memories")
1205
+ .select("*")
1206
+ .eq("project", project)
1207
+ .eq("resolved", false);
1208
+ if (memoriesError)
1209
+ fail(`Failed to load memories: ${memoriesError.message}`);
1210
+ const { data: executionLog, error: executionLogError } = await supabase
1211
+ .from("execution_log")
1212
+ .select("*")
1213
+ .eq("project", project);
1214
+ if (executionLogError)
1215
+ fail(`Failed to load execution log: ${executionLogError.message}`);
1216
+ const memoryResults = memories ?? [];
1217
+ const memoryIds = memoryResults.map((m) => m.id);
1218
+ let memoryLinks = [];
1219
+ if (memoryIds.length > 0) {
1220
+ const idList = memoryIds.join(",");
1221
+ const { data: links, error: linksError } = await supabase
1222
+ .from("memory_links")
1223
+ .select("*")
1224
+ .or(`source_id.in.(${idList}),target_id.in.(${idList})`);
1225
+ if (linksError)
1226
+ fail(`Failed to load memory links: ${linksError.message}`);
1227
+ memoryLinks = links ?? [];
1228
+ }
1229
+ const exportData = {
1230
+ project,
1231
+ exported_at: new Date().toISOString(),
1232
+ memories: memoryResults,
1233
+ execution_log: executionLog ?? [],
1234
+ memory_links: memoryLinks,
1235
+ };
1236
+ const outputFilename = filename ?? `${project}-memories.json`;
1237
+ nodeFs.writeFileSync(outputFilename, JSON.stringify(exportData, null, 2), "utf-8");
1238
+ console.log(`Exported ${memoryResults.length} memories to ${outputFilename}`);
1239
+ }
1240
+ // --- cm import -------------------------------------------------------------
1241
+ async function cmdImport(project, filename) {
1242
+ if (!nodeFs.existsSync(filename)) {
1243
+ console.log(`File not found: ${filename}`);
1244
+ return;
1245
+ }
1246
+ let parsed;
1247
+ try {
1248
+ parsed = JSON.parse(nodeFs.readFileSync(filename, "utf-8"));
1249
+ }
1250
+ catch (error) {
1251
+ fail(`Failed to parse ${filename}: ${error instanceof Error ? error.message : String(error)}`);
1252
+ }
1253
+ const memories = Array.isArray(parsed.memories) ? parsed.memories : [];
1254
+ const executionLogEntries = Array.isArray(parsed.execution_log) ? parsed.execution_log : [];
1255
+ // memory_links are intentionally skipped — they get rebuilt by semantic
1256
+ // linking the next time cm save runs against the imported memories.
1257
+ let importedCount = 0;
1258
+ for (const memory of memories) {
1259
+ const { error } = await supabase.from("memories").insert({
1260
+ project,
1261
+ type: memory.type,
1262
+ content: memory.content,
1263
+ device_id: getDeviceId(),
1264
+ });
1265
+ if (!error)
1266
+ importedCount++;
1267
+ }
1268
+ for (const entry of executionLogEntries) {
1269
+ await supabase.from("execution_log").insert({
1270
+ project,
1271
+ problem: entry.problem,
1272
+ solution: entry.solution ?? null,
1273
+ resolved: entry.resolved ?? false,
1274
+ device_id: getDeviceId(),
1275
+ });
1276
+ }
1277
+ console.log(`Imported ${importedCount} memories from ${filename}`);
1278
+ }
870
1279
  // --- main --------------------------------------------------------------
871
1280
  async function main() {
872
1281
  const [, , command, ...args] = process.argv;
@@ -969,6 +1378,39 @@ async function main() {
969
1378
  await cmdContext(project, split.rest.join(" "));
970
1379
  break;
971
1380
  }
1381
+ case "session-end": {
1382
+ await cmdSessionEndEntry(args);
1383
+ break;
1384
+ }
1385
+ case "doctor": {
1386
+ await cmdDoctor();
1387
+ break;
1388
+ }
1389
+ case "undo": {
1390
+ const split = splitOptionalProject(args, 0);
1391
+ if (!split)
1392
+ usage();
1393
+ const project = requireProject(split.project);
1394
+ await cmdUndo(project);
1395
+ break;
1396
+ }
1397
+ case "export": {
1398
+ if (args.length > 1)
1399
+ usage();
1400
+ const [filename] = args;
1401
+ const project = requireProject(null);
1402
+ await cmdExport(project, filename);
1403
+ break;
1404
+ }
1405
+ case "import": {
1406
+ const split = splitOptionalProject(args, 1);
1407
+ if (!split)
1408
+ usage();
1409
+ const project = requireProject(split.project);
1410
+ const [filename] = split.rest;
1411
+ await cmdImport(project, filename);
1412
+ break;
1413
+ }
972
1414
  case "help": {
973
1415
  cmdHelp();
974
1416
  break;
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { analyzeCodebase, getFileSummary, findDependencies } from "./ast-index.js";
6
6
  import { getDeviceId } from "./device.js";
7
- import { applyContextBudget, buildSummaryContent, clusterMemories, computeDecayScore, computeMemoryLinks, decideSaveAction, estimateTokens, } from "./scoring.js";
7
+ import { applyContextBudget, buildSummaryContent, clusterMemories, computeDecayScore, computeMemoryLinks, decideSaveAction, estimateTokens, indicatesFailedApproach, isAuthRelatedFile, isCodeFile, isQualityMessage, } from "./scoring.js";
8
8
  import { supabase } from "./storage.js";
9
9
  const CONTEXT_BUDGET_TOKENS = 2000;
10
10
  const COMPRESSION_THRESHOLD = 20;
@@ -312,6 +312,80 @@ server.registerTool("find_dependencies", {
312
312
  structuredContent: deps,
313
313
  };
314
314
  });
315
+ // --- session_end -----------------------------------------------------------
316
+ server.registerTool("session_end", {
317
+ title: "Session End",
318
+ description: "Capture what happened during a coding session and automatically extract memories: unsolved errors, failed approaches, a summary decision, auth-file touches, and an AST re-index.",
319
+ inputSchema: {
320
+ project: z.string().describe("Project identifier"),
321
+ files_touched: z.array(z.string()).describe("Files read or modified during the session"),
322
+ errors_encountered: z.array(z.string()).describe("Error messages encountered during the session"),
323
+ approaches_tried: z
324
+ .array(z.string())
325
+ .optional()
326
+ .describe("Things that were attempted during the session"),
327
+ summary: z.string().optional().describe("One sentence describing what was accomplished"),
328
+ },
329
+ }, async ({ project, files_touched, errors_encountered, approaches_tried, summary }) => {
330
+ let memoriesSaved = 0;
331
+ let errorsLogged = 0;
332
+ for (const problem of errors_encountered) {
333
+ const { error } = await supabase.from("execution_log").insert({
334
+ project,
335
+ problem,
336
+ solution: null,
337
+ resolved: false,
338
+ device_id: getDeviceId(),
339
+ });
340
+ if (!error)
341
+ errorsLogged++;
342
+ }
343
+ for (const approach of approaches_tried ?? []) {
344
+ if (!indicatesFailedApproach(approach))
345
+ continue;
346
+ const { error } = await supabase.from("memories").insert({
347
+ project,
348
+ type: "rejection",
349
+ content: approach,
350
+ device_id: getDeviceId(),
351
+ });
352
+ if (!error)
353
+ memoriesSaved++;
354
+ }
355
+ if (summary && isQualityMessage(summary)) {
356
+ const { error } = await supabase.from("memories").insert({
357
+ project,
358
+ type: "decision",
359
+ content: summary.trim(),
360
+ device_id: getDeviceId(),
361
+ });
362
+ if (!error)
363
+ memoriesSaved++;
364
+ }
365
+ const authFiles = files_touched.filter(isAuthRelatedFile);
366
+ if (authFiles.length > 0) {
367
+ const { error } = await supabase.from("memories").insert({
368
+ project,
369
+ type: "discovery",
370
+ content: `session touched auth files: ${authFiles.join(", ")}`,
371
+ device_id: getDeviceId(),
372
+ });
373
+ if (!error)
374
+ memoriesSaved++;
375
+ }
376
+ if (files_touched.some(isCodeFile)) {
377
+ analyzeCodebase(process.cwd());
378
+ }
379
+ const message = `Session captured. ${memoriesSaved} memories saved.`;
380
+ return {
381
+ content: [{ type: "text", text: message }],
382
+ structuredContent: {
383
+ memories_saved: memoriesSaved,
384
+ errors_logged: errorsLogged,
385
+ message,
386
+ },
387
+ };
388
+ });
315
389
  // --- transport -------------------------------------------------------------
316
390
  async function main() {
317
391
  const transport = new StdioServerTransport();