stackmem 1.0.6 → 1.0.8
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/cli.js +253 -2
- package/package.json +1 -1
- package/src/cli.ts +269 -2
package/dist/cli.js
CHANGED
|
@@ -36,6 +36,10 @@ function usage() {
|
|
|
36
36
|
cm init <project> <path>
|
|
37
37
|
cm context [--project <name>] <task>
|
|
38
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>
|
|
39
43
|
cm help
|
|
40
44
|
|
|
41
45
|
[project] is optional if a .stackmem file exists in the current
|
|
@@ -107,6 +111,26 @@ function cmdHelp() {
|
|
|
107
111
|
(project, summary, errors_encountered, files_touched,
|
|
108
112
|
approaches_tried) when stdin is piped rather than a terminal.
|
|
109
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.
|
|
133
|
+
|
|
110
134
|
cm help
|
|
111
135
|
Show this help message.`);
|
|
112
136
|
}
|
|
@@ -696,7 +720,7 @@ for (const line of isFirstCommit ? [] : lines) {
|
|
|
696
720
|
savedItems.push(\`discovery: \${content}\`);
|
|
697
721
|
}
|
|
698
722
|
|
|
699
|
-
if (
|
|
723
|
+
if (/^\\+\\s*(\\/\\/|#)\\s*(TODO:|FIXME:|HACK:|XXX:)/.test(line)) {
|
|
700
724
|
const truncated = codeLine.trim().slice(0, 100);
|
|
701
725
|
const content = \`open issue from commit: \${truncated}\`;
|
|
702
726
|
cmSave("discovery", content);
|
|
@@ -859,7 +883,7 @@ async function cmdInit(project, targetPath) {
|
|
|
859
883
|
console.log(`stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`);
|
|
860
884
|
}
|
|
861
885
|
else {
|
|
862
|
-
console.log("✗ Backend connection failed — run 'cm doctor'
|
|
886
|
+
console.log("✗ Backend connection failed — run 'cm doctor'");
|
|
863
887
|
}
|
|
864
888
|
// Written last and deliberately: if a commit (and hence the post-commit
|
|
865
889
|
// hook) fires anywhere during init, it must still see whatever project
|
|
@@ -1054,6 +1078,204 @@ async function cmdSessionEndEntry(args) {
|
|
|
1054
1078
|
approaches: input.approaches,
|
|
1055
1079
|
});
|
|
1056
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
|
+
}
|
|
1057
1279
|
// --- main --------------------------------------------------------------
|
|
1058
1280
|
async function main() {
|
|
1059
1281
|
const [, , command, ...args] = process.argv;
|
|
@@ -1160,6 +1382,35 @@ async function main() {
|
|
|
1160
1382
|
await cmdSessionEndEntry(args);
|
|
1161
1383
|
break;
|
|
1162
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
|
+
}
|
|
1163
1414
|
case "help": {
|
|
1164
1415
|
cmdHelp();
|
|
1165
1416
|
break;
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -54,6 +54,10 @@ function usage(): never {
|
|
|
54
54
|
cm init <project> <path>
|
|
55
55
|
cm context [--project <name>] <task>
|
|
56
56
|
cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
|
|
57
|
+
cm doctor
|
|
58
|
+
cm undo [project]
|
|
59
|
+
cm export [filename]
|
|
60
|
+
cm import [project] <filename>
|
|
57
61
|
cm help
|
|
58
62
|
|
|
59
63
|
[project] is optional if a .stackmem file exists in the current
|
|
@@ -126,6 +130,26 @@ function cmdHelp() {
|
|
|
126
130
|
(project, summary, errors_encountered, files_touched,
|
|
127
131
|
approaches_tried) when stdin is piped rather than a terminal.
|
|
128
132
|
|
|
133
|
+
cm doctor
|
|
134
|
+
Check every integration point in the current directory — backend
|
|
135
|
+
connection, MCP registration, .stackmem, git hook, and memory
|
|
136
|
+
count — and report what's working and what's broken.
|
|
137
|
+
|
|
138
|
+
cm undo [project]
|
|
139
|
+
Delete the most recently saved unresolved memory (and any
|
|
140
|
+
memory_links referencing it) for a project.
|
|
141
|
+
|
|
142
|
+
cm export [filename]
|
|
143
|
+
Export all unresolved memories, execution_log entries, and
|
|
144
|
+
memory_links for a project to a JSON file. Defaults to
|
|
145
|
+
<project>-memories.json.
|
|
146
|
+
|
|
147
|
+
cm import [project] <filename>
|
|
148
|
+
Import memories and execution_log entries from a JSON file
|
|
149
|
+
produced by 'cm export'. Inserted under the current device_id
|
|
150
|
+
and project; memory_links are skipped and get rebuilt by
|
|
151
|
+
semantic linking the next time cm save runs.
|
|
152
|
+
|
|
129
153
|
cm help
|
|
130
154
|
Show this help message.`);
|
|
131
155
|
}
|
|
@@ -794,7 +818,7 @@ for (const line of isFirstCommit ? [] : lines) {
|
|
|
794
818
|
savedItems.push(\`discovery: \${content}\`);
|
|
795
819
|
}
|
|
796
820
|
|
|
797
|
-
if (
|
|
821
|
+
if (/^\\+\\s*(\\/\\/|#)\\s*(TODO:|FIXME:|HACK:|XXX:)/.test(line)) {
|
|
798
822
|
const truncated = codeLine.trim().slice(0, 100);
|
|
799
823
|
const content = \`open issue from commit: \${truncated}\`;
|
|
800
824
|
cmSave("discovery", content);
|
|
@@ -984,7 +1008,7 @@ async function cmdInit(project: string, targetPath: string) {
|
|
|
984
1008
|
`stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`
|
|
985
1009
|
);
|
|
986
1010
|
} else {
|
|
987
|
-
console.log("✗ Backend connection failed — run 'cm doctor'
|
|
1011
|
+
console.log("✗ Backend connection failed — run 'cm doctor'");
|
|
988
1012
|
}
|
|
989
1013
|
|
|
990
1014
|
// Written last and deliberately: if a commit (and hence the post-commit
|
|
@@ -1208,6 +1232,223 @@ async function cmdSessionEndEntry(args: string[]) {
|
|
|
1208
1232
|
});
|
|
1209
1233
|
}
|
|
1210
1234
|
|
|
1235
|
+
// --- cm doctor ---------------------------------------------------------
|
|
1236
|
+
|
|
1237
|
+
async function cmdDoctor() {
|
|
1238
|
+
let issues = 0;
|
|
1239
|
+
const project = readProjectFromFile();
|
|
1240
|
+
|
|
1241
|
+
// 1. Backend connection
|
|
1242
|
+
const backendOk = await verifyBackendConnection(project ?? "");
|
|
1243
|
+
if (backendOk) {
|
|
1244
|
+
console.log("✓ Backend connection verified");
|
|
1245
|
+
} else {
|
|
1246
|
+
console.log("✗ Backend connection failed — check your network");
|
|
1247
|
+
issues++;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// 2. MCP server registration
|
|
1251
|
+
const claudeConfigPath = nodePath.join(nodeOs.homedir(), ".claude.json");
|
|
1252
|
+
let mcpRegistered = false;
|
|
1253
|
+
if (nodeFs.existsSync(claudeConfigPath)) {
|
|
1254
|
+
try {
|
|
1255
|
+
const config = JSON.parse(nodeFs.readFileSync(claudeConfigPath, "utf-8"));
|
|
1256
|
+
mcpRegistered = Boolean(config?.mcpServers?.stackmem);
|
|
1257
|
+
} catch {
|
|
1258
|
+
mcpRegistered = false;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (mcpRegistered) {
|
|
1262
|
+
console.log("✓ MCP server registered with Claude Code");
|
|
1263
|
+
} else {
|
|
1264
|
+
console.log("✗ MCP server not registered — run 'cm init <project> .'");
|
|
1265
|
+
issues++;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// 3. Project file
|
|
1269
|
+
if (project) {
|
|
1270
|
+
console.log(`✓ Project file found: ${project}`);
|
|
1271
|
+
} else {
|
|
1272
|
+
console.log("✗ No .stackmem found — run 'cm init <project> .'");
|
|
1273
|
+
issues++;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// 4. Git hook
|
|
1277
|
+
const gitDir = nodePath.join(process.cwd(), ".git");
|
|
1278
|
+
if (!nodeFs.existsSync(gitDir)) {
|
|
1279
|
+
console.log("✗ No git repo found in current directory");
|
|
1280
|
+
issues++;
|
|
1281
|
+
} else {
|
|
1282
|
+
const hookPath = nodePath.join(gitDir, "hooks", "post-commit");
|
|
1283
|
+
let hookExecutable = false;
|
|
1284
|
+
if (nodeFs.existsSync(hookPath)) {
|
|
1285
|
+
try {
|
|
1286
|
+
nodeFs.accessSync(hookPath, nodeFs.constants.X_OK);
|
|
1287
|
+
hookExecutable = true;
|
|
1288
|
+
} catch {
|
|
1289
|
+
hookExecutable = false;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (hookExecutable) {
|
|
1293
|
+
console.log("✓ Git hook installed");
|
|
1294
|
+
} else {
|
|
1295
|
+
console.log("✗ Git hook not installed — run 'cm init <project> .'");
|
|
1296
|
+
issues++;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// 5. Memory count
|
|
1301
|
+
if (project) {
|
|
1302
|
+
const { count, error } = await supabase
|
|
1303
|
+
.from("memories")
|
|
1304
|
+
.select("id", { count: "exact", head: true })
|
|
1305
|
+
.eq("project", project)
|
|
1306
|
+
.eq("resolved", false);
|
|
1307
|
+
|
|
1308
|
+
if (!error && (count ?? 0) > 0) {
|
|
1309
|
+
console.log(`✓ ${count} memories stored for "${project}"`);
|
|
1310
|
+
} else {
|
|
1311
|
+
console.log(`✗ No memories yet — make a commit to start capturing`);
|
|
1312
|
+
issues++;
|
|
1313
|
+
}
|
|
1314
|
+
} else {
|
|
1315
|
+
console.log(`✗ No memories yet — make a commit to start capturing`);
|
|
1316
|
+
issues++;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
console.log("");
|
|
1320
|
+
if (issues === 0) {
|
|
1321
|
+
console.log("All systems operational.");
|
|
1322
|
+
} else {
|
|
1323
|
+
console.log(`${issues} issue(s) found. See above.`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// --- cm undo -------------------------------------------------------------
|
|
1328
|
+
|
|
1329
|
+
async function cmdUndo(project: string) {
|
|
1330
|
+
const { data, error } = await supabase
|
|
1331
|
+
.from("memories")
|
|
1332
|
+
.select("*")
|
|
1333
|
+
.eq("project", project)
|
|
1334
|
+
.eq("resolved", false)
|
|
1335
|
+
.order("created_at", { ascending: false })
|
|
1336
|
+
.limit(1);
|
|
1337
|
+
|
|
1338
|
+
if (error) fail(`Failed to load most recent memory: ${error.message}`);
|
|
1339
|
+
|
|
1340
|
+
const memory = (data ?? [])[0];
|
|
1341
|
+
if (!memory) {
|
|
1342
|
+
console.log("Nothing to undo.");
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const { error: linksError } = await supabase
|
|
1347
|
+
.from("memory_links")
|
|
1348
|
+
.delete()
|
|
1349
|
+
.or(`source_id.eq.${memory.id},target_id.eq.${memory.id}`);
|
|
1350
|
+
|
|
1351
|
+
if (linksError) fail(`Failed to delete memory links: ${linksError.message}`);
|
|
1352
|
+
|
|
1353
|
+
const { error: deleteError } = await supabase.from("memories").delete().eq("id", memory.id);
|
|
1354
|
+
|
|
1355
|
+
if (deleteError) fail(`Failed to delete memory: ${deleteError.message}`);
|
|
1356
|
+
|
|
1357
|
+
console.log(`Undone: [${memory.type}] ${memory.content}`);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// --- cm export -------------------------------------------------------------
|
|
1361
|
+
|
|
1362
|
+
async function cmdExport(project: string, filename?: string) {
|
|
1363
|
+
const { data: memories, error: memoriesError } = await supabase
|
|
1364
|
+
.from("memories")
|
|
1365
|
+
.select("*")
|
|
1366
|
+
.eq("project", project)
|
|
1367
|
+
.eq("resolved", false);
|
|
1368
|
+
|
|
1369
|
+
if (memoriesError) fail(`Failed to load memories: ${memoriesError.message}`);
|
|
1370
|
+
|
|
1371
|
+
const { data: executionLog, error: executionLogError } = await supabase
|
|
1372
|
+
.from("execution_log")
|
|
1373
|
+
.select("*")
|
|
1374
|
+
.eq("project", project);
|
|
1375
|
+
|
|
1376
|
+
if (executionLogError) fail(`Failed to load execution log: ${executionLogError.message}`);
|
|
1377
|
+
|
|
1378
|
+
const memoryResults = memories ?? [];
|
|
1379
|
+
const memoryIds = memoryResults.map((m) => m.id);
|
|
1380
|
+
|
|
1381
|
+
let memoryLinks: unknown[] = [];
|
|
1382
|
+
if (memoryIds.length > 0) {
|
|
1383
|
+
const idList = memoryIds.join(",");
|
|
1384
|
+
const { data: links, error: linksError } = await supabase
|
|
1385
|
+
.from("memory_links")
|
|
1386
|
+
.select("*")
|
|
1387
|
+
.or(`source_id.in.(${idList}),target_id.in.(${idList})`);
|
|
1388
|
+
|
|
1389
|
+
if (linksError) fail(`Failed to load memory links: ${linksError.message}`);
|
|
1390
|
+
memoryLinks = links ?? [];
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const exportData = {
|
|
1394
|
+
project,
|
|
1395
|
+
exported_at: new Date().toISOString(),
|
|
1396
|
+
memories: memoryResults,
|
|
1397
|
+
execution_log: executionLog ?? [],
|
|
1398
|
+
memory_links: memoryLinks,
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
const outputFilename = filename ?? `${project}-memories.json`;
|
|
1402
|
+
nodeFs.writeFileSync(outputFilename, JSON.stringify(exportData, null, 2), "utf-8");
|
|
1403
|
+
|
|
1404
|
+
console.log(`Exported ${memoryResults.length} memories to ${outputFilename}`);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// --- cm import -------------------------------------------------------------
|
|
1408
|
+
|
|
1409
|
+
async function cmdImport(project: string, filename: string) {
|
|
1410
|
+
if (!nodeFs.existsSync(filename)) {
|
|
1411
|
+
console.log(`File not found: ${filename}`);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
let parsed: any;
|
|
1416
|
+
try {
|
|
1417
|
+
parsed = JSON.parse(nodeFs.readFileSync(filename, "utf-8"));
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
fail(`Failed to parse ${filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const memories = Array.isArray(parsed.memories) ? parsed.memories : [];
|
|
1423
|
+
const executionLogEntries = Array.isArray(parsed.execution_log) ? parsed.execution_log : [];
|
|
1424
|
+
|
|
1425
|
+
// memory_links are intentionally skipped — they get rebuilt by semantic
|
|
1426
|
+
// linking the next time cm save runs against the imported memories.
|
|
1427
|
+
|
|
1428
|
+
let importedCount = 0;
|
|
1429
|
+
for (const memory of memories) {
|
|
1430
|
+
const { error } = await supabase.from("memories").insert({
|
|
1431
|
+
project,
|
|
1432
|
+
type: memory.type,
|
|
1433
|
+
content: memory.content,
|
|
1434
|
+
device_id: getDeviceId(),
|
|
1435
|
+
});
|
|
1436
|
+
if (!error) importedCount++;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
for (const entry of executionLogEntries) {
|
|
1440
|
+
await supabase.from("execution_log").insert({
|
|
1441
|
+
project,
|
|
1442
|
+
problem: entry.problem,
|
|
1443
|
+
solution: entry.solution ?? null,
|
|
1444
|
+
resolved: entry.resolved ?? false,
|
|
1445
|
+
device_id: getDeviceId(),
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
console.log(`Imported ${importedCount} memories from ${filename}`);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1211
1452
|
// --- main --------------------------------------------------------------
|
|
1212
1453
|
|
|
1213
1454
|
async function main() {
|
|
@@ -1307,6 +1548,32 @@ async function main() {
|
|
|
1307
1548
|
await cmdSessionEndEntry(args);
|
|
1308
1549
|
break;
|
|
1309
1550
|
}
|
|
1551
|
+
case "doctor": {
|
|
1552
|
+
await cmdDoctor();
|
|
1553
|
+
break;
|
|
1554
|
+
}
|
|
1555
|
+
case "undo": {
|
|
1556
|
+
const split = splitOptionalProject(args, 0);
|
|
1557
|
+
if (!split) usage();
|
|
1558
|
+
const project = requireProject(split.project);
|
|
1559
|
+
await cmdUndo(project);
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
case "export": {
|
|
1563
|
+
if (args.length > 1) usage();
|
|
1564
|
+
const [filename] = args;
|
|
1565
|
+
const project = requireProject(null);
|
|
1566
|
+
await cmdExport(project, filename);
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
case "import": {
|
|
1570
|
+
const split = splitOptionalProject(args, 1);
|
|
1571
|
+
if (!split) usage();
|
|
1572
|
+
const project = requireProject(split.project);
|
|
1573
|
+
const [filename] = split.rest;
|
|
1574
|
+
await cmdImport(project, filename);
|
|
1575
|
+
break;
|
|
1576
|
+
}
|
|
1310
1577
|
case "help": {
|
|
1311
1578
|
cmdHelp();
|
|
1312
1579
|
break;
|