engram-mcp-server 1.2.8 → 1.4.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.
Files changed (288) hide show
  1. package/README.md +131 -24
  2. package/dist/constants.d.ts +12 -1
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +19 -1
  5. package/dist/constants.js.map +1 -1
  6. package/dist/database.d.ts +9 -0
  7. package/dist/database.d.ts.map +1 -1
  8. package/dist/database.js +16 -0
  9. package/dist/database.js.map +1 -1
  10. package/dist/index.js +16 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/installer/config-writer.d.ts +11 -2
  13. package/dist/installer/config-writer.d.ts.map +1 -1
  14. package/dist/installer/config-writer.js +28 -3
  15. package/dist/installer/config-writer.js.map +1 -1
  16. package/dist/installer/ide-detector.d.ts.map +1 -1
  17. package/dist/installer/ide-detector.js +10 -6
  18. package/dist/installer/ide-detector.js.map +1 -1
  19. package/dist/installer/index.d.ts.map +1 -1
  20. package/dist/installer/index.js +156 -23
  21. package/dist/installer/index.js.map +1 -1
  22. package/dist/repositories/changes.repo.d.ts.map +1 -1
  23. package/dist/repositories/changes.repo.js +5 -3
  24. package/dist/repositories/changes.repo.js.map +1 -1
  25. package/dist/repositories/config.repo.d.ts +5 -0
  26. package/dist/repositories/config.repo.d.ts.map +1 -1
  27. package/dist/repositories/config.repo.js +8 -0
  28. package/dist/repositories/config.repo.js.map +1 -1
  29. package/dist/repositories/decisions.repo.d.ts +8 -0
  30. package/dist/repositories/decisions.repo.d.ts.map +1 -1
  31. package/dist/repositories/decisions.repo.js +23 -0
  32. package/dist/repositories/decisions.repo.js.map +1 -1
  33. package/dist/repositories/file-notes.repo.d.ts +9 -0
  34. package/dist/repositories/file-notes.repo.d.ts.map +1 -1
  35. package/dist/repositories/file-notes.repo.js +22 -4
  36. package/dist/repositories/file-notes.repo.js.map +1 -1
  37. package/dist/response.d.ts +1 -0
  38. package/dist/response.d.ts.map +1 -1
  39. package/dist/response.js.map +1 -1
  40. package/dist/services/index.d.ts +2 -0
  41. package/dist/services/index.d.ts.map +1 -1
  42. package/dist/services/index.js +1 -0
  43. package/dist/services/index.js.map +1 -1
  44. package/dist/services/update.service.d.ts +29 -0
  45. package/dist/services/update.service.d.ts.map +1 -0
  46. package/dist/services/update.service.js +166 -0
  47. package/dist/services/update.service.js.map +1 -0
  48. package/dist/tools/backup.d.ts.map +1 -1
  49. package/dist/tools/backup.js +20 -45
  50. package/dist/tools/backup.js.map +1 -1
  51. package/dist/tools/changes.d.ts.map +1 -1
  52. package/dist/tools/changes.js +24 -33
  53. package/dist/tools/changes.js.map +1 -1
  54. package/dist/tools/compaction.d.ts.map +1 -1
  55. package/dist/tools/compaction.js +19 -72
  56. package/dist/tools/compaction.js.map +1 -1
  57. package/dist/tools/conventions.d.ts.map +1 -1
  58. package/dist/tools/conventions.js +9 -18
  59. package/dist/tools/conventions.js.map +1 -1
  60. package/dist/tools/decisions.d.ts.map +1 -1
  61. package/dist/tools/decisions.js +71 -45
  62. package/dist/tools/decisions.js.map +1 -1
  63. package/dist/tools/export-import.d.ts.map +1 -1
  64. package/dist/tools/export-import.js +21 -35
  65. package/dist/tools/export-import.js.map +1 -1
  66. package/dist/tools/file-notes.d.ts.map +1 -1
  67. package/dist/tools/file-notes.js +58 -36
  68. package/dist/tools/file-notes.js.map +1 -1
  69. package/dist/tools/intelligence.d.ts.map +1 -1
  70. package/dist/tools/intelligence.js +109 -164
  71. package/dist/tools/intelligence.js.map +1 -1
  72. package/dist/tools/milestones.d.ts.map +1 -1
  73. package/dist/tools/milestones.js +6 -10
  74. package/dist/tools/milestones.js.map +1 -1
  75. package/dist/tools/scheduler.d.ts.map +1 -1
  76. package/dist/tools/scheduler.js +42 -89
  77. package/dist/tools/scheduler.js.map +1 -1
  78. package/dist/tools/sessions.d.ts.map +1 -1
  79. package/dist/tools/sessions.js +131 -226
  80. package/dist/tools/sessions.js.map +1 -1
  81. package/dist/tools/stats.d.ts.map +1 -1
  82. package/dist/tools/stats.js +169 -8
  83. package/dist/tools/stats.js.map +1 -1
  84. package/dist/tools/tasks.d.ts.map +1 -1
  85. package/dist/tools/tasks.js +9 -20
  86. package/dist/tools/tasks.js.map +1 -1
  87. package/dist/utils.d.ts +9 -0
  88. package/dist/utils.d.ts.map +1 -1
  89. package/dist/utils.js +18 -0
  90. package/dist/utils.js.map +1 -1
  91. package/package.json +60 -52
  92. package/scripts/inject-release-notes.js +66 -0
  93. package/dist/installer.d.ts +0 -2
  94. package/dist/installer.d.ts.map +0 -1
  95. package/dist/installer.js +0 -315
  96. package/dist/installer.js.map +0 -1
  97. package/dist/src/constants.d.ts +0 -21
  98. package/dist/src/constants.d.ts.map +0 -1
  99. package/dist/src/constants.js +0 -81
  100. package/dist/src/constants.js.map +0 -1
  101. package/dist/src/database.d.ts +0 -32
  102. package/dist/src/database.d.ts.map +0 -1
  103. package/dist/src/database.js +0 -143
  104. package/dist/src/database.js.map +0 -1
  105. package/dist/src/errors.d.ts +0 -40
  106. package/dist/src/errors.d.ts.map +0 -1
  107. package/dist/src/errors.js +0 -63
  108. package/dist/src/errors.js.map +0 -1
  109. package/dist/src/index.d.ts +0 -3
  110. package/dist/src/index.d.ts.map +0 -1
  111. package/dist/src/index.js +0 -89
  112. package/dist/src/index.js.map +0 -1
  113. package/dist/src/installer/config-writer.d.ts +0 -25
  114. package/dist/src/installer/config-writer.d.ts.map +0 -1
  115. package/dist/src/installer/config-writer.js +0 -86
  116. package/dist/src/installer/config-writer.js.map +0 -1
  117. package/dist/src/installer/ide-configs.d.ts +0 -19
  118. package/dist/src/installer/ide-configs.d.ts.map +0 -1
  119. package/dist/src/installer/ide-configs.js +0 -131
  120. package/dist/src/installer/ide-configs.js.map +0 -1
  121. package/dist/src/installer/ide-detector.d.ts +0 -6
  122. package/dist/src/installer/ide-detector.d.ts.map +0 -1
  123. package/dist/src/installer/ide-detector.js +0 -45
  124. package/dist/src/installer/ide-detector.js.map +0 -1
  125. package/dist/src/installer/index.d.ts +0 -2
  126. package/dist/src/installer/index.d.ts.map +0 -1
  127. package/dist/src/installer/index.js +0 -229
  128. package/dist/src/installer/index.js.map +0 -1
  129. package/dist/src/logger.d.ts +0 -10
  130. package/dist/src/logger.d.ts.map +0 -1
  131. package/dist/src/logger.js +0 -51
  132. package/dist/src/logger.js.map +0 -1
  133. package/dist/src/migrations.d.ts +0 -4
  134. package/dist/src/migrations.d.ts.map +0 -1
  135. package/dist/src/migrations.js +0 -343
  136. package/dist/src/migrations.js.map +0 -1
  137. package/dist/src/repositories/changes.repo.d.ts +0 -35
  138. package/dist/src/repositories/changes.repo.d.ts.map +0 -1
  139. package/dist/src/repositories/changes.repo.js +0 -53
  140. package/dist/src/repositories/changes.repo.js.map +0 -1
  141. package/dist/src/repositories/config.repo.d.ts +0 -11
  142. package/dist/src/repositories/config.repo.d.ts.map +0 -1
  143. package/dist/src/repositories/config.repo.js +0 -38
  144. package/dist/src/repositories/config.repo.js.map +0 -1
  145. package/dist/src/repositories/conventions.repo.d.ts +0 -15
  146. package/dist/src/repositories/conventions.repo.d.ts.map +0 -1
  147. package/dist/src/repositories/conventions.repo.js +0 -39
  148. package/dist/src/repositories/conventions.repo.js.map +0 -1
  149. package/dist/src/repositories/decisions.repo.d.ts +0 -19
  150. package/dist/src/repositories/decisions.repo.d.ts.map +0 -1
  151. package/dist/src/repositories/decisions.repo.js +0 -48
  152. package/dist/src/repositories/decisions.repo.js.map +0 -1
  153. package/dist/src/repositories/events.repo.d.ts +0 -34
  154. package/dist/src/repositories/events.repo.d.ts.map +0 -1
  155. package/dist/src/repositories/events.repo.js +0 -71
  156. package/dist/src/repositories/events.repo.js.map +0 -1
  157. package/dist/src/repositories/file-notes.repo.d.ts +0 -26
  158. package/dist/src/repositories/file-notes.repo.d.ts.map +0 -1
  159. package/dist/src/repositories/file-notes.repo.js +0 -55
  160. package/dist/src/repositories/file-notes.repo.js.map +0 -1
  161. package/dist/src/repositories/index.d.ts +0 -38
  162. package/dist/src/repositories/index.d.ts.map +0 -1
  163. package/dist/src/repositories/index.js +0 -41
  164. package/dist/src/repositories/index.js.map +0 -1
  165. package/dist/src/repositories/milestones.repo.d.ts +0 -10
  166. package/dist/src/repositories/milestones.repo.d.ts.map +0 -1
  167. package/dist/src/repositories/milestones.repo.js +0 -20
  168. package/dist/src/repositories/milestones.repo.js.map +0 -1
  169. package/dist/src/repositories/sessions.repo.d.ts +0 -27
  170. package/dist/src/repositories/sessions.repo.d.ts.map +0 -1
  171. package/dist/src/repositories/sessions.repo.js +0 -61
  172. package/dist/src/repositories/sessions.repo.js.map +0 -1
  173. package/dist/src/repositories/snapshot.repo.d.ts +0 -11
  174. package/dist/src/repositories/snapshot.repo.d.ts.map +0 -1
  175. package/dist/src/repositories/snapshot.repo.js +0 -17
  176. package/dist/src/repositories/snapshot.repo.js.map +0 -1
  177. package/dist/src/repositories/tasks.repo.d.ts +0 -40
  178. package/dist/src/repositories/tasks.repo.d.ts.map +0 -1
  179. package/dist/src/repositories/tasks.repo.js +0 -90
  180. package/dist/src/repositories/tasks.repo.js.map +0 -1
  181. package/dist/src/response.d.ts +0 -28
  182. package/dist/src/response.d.ts.map +0 -1
  183. package/dist/src/response.js +0 -38
  184. package/dist/src/response.js.map +0 -1
  185. package/dist/src/scripts/install-hooks.d.ts +0 -3
  186. package/dist/src/scripts/install-hooks.d.ts.map +0 -1
  187. package/dist/src/scripts/install-hooks.js +0 -89
  188. package/dist/src/scripts/install-hooks.js.map +0 -1
  189. package/dist/src/services/compaction.service.d.ts +0 -27
  190. package/dist/src/services/compaction.service.d.ts.map +0 -1
  191. package/dist/src/services/compaction.service.js +0 -93
  192. package/dist/src/services/compaction.service.js.map +0 -1
  193. package/dist/src/services/event-trigger.service.d.ts +0 -24
  194. package/dist/src/services/event-trigger.service.d.ts.map +0 -1
  195. package/dist/src/services/event-trigger.service.js +0 -60
  196. package/dist/src/services/event-trigger.service.js.map +0 -1
  197. package/dist/src/services/git.service.d.ts +0 -20
  198. package/dist/src/services/git.service.d.ts.map +0 -1
  199. package/dist/src/services/git.service.js +0 -88
  200. package/dist/src/services/git.service.js.map +0 -1
  201. package/dist/src/services/index.d.ts +0 -5
  202. package/dist/src/services/index.d.ts.map +0 -1
  203. package/dist/src/services/index.js +0 -8
  204. package/dist/src/services/index.js.map +0 -1
  205. package/dist/src/services/project-scan.service.d.ts +0 -19
  206. package/dist/src/services/project-scan.service.d.ts.map +0 -1
  207. package/dist/src/services/project-scan.service.js +0 -66
  208. package/dist/src/services/project-scan.service.js.map +0 -1
  209. package/dist/src/tools/backup.d.ts +0 -3
  210. package/dist/src/tools/backup.d.ts.map +0 -1
  211. package/dist/src/tools/backup.js +0 -191
  212. package/dist/src/tools/backup.js.map +0 -1
  213. package/dist/src/tools/changes.d.ts +0 -3
  214. package/dist/src/tools/changes.d.ts.map +0 -1
  215. package/dist/src/tools/changes.js +0 -97
  216. package/dist/src/tools/changes.js.map +0 -1
  217. package/dist/src/tools/compaction.d.ts +0 -3
  218. package/dist/src/tools/compaction.d.ts.map +0 -1
  219. package/dist/src/tools/compaction.js +0 -151
  220. package/dist/src/tools/compaction.js.map +0 -1
  221. package/dist/src/tools/conventions.d.ts +0 -3
  222. package/dist/src/tools/conventions.d.ts.map +0 -1
  223. package/dist/src/tools/conventions.js +0 -121
  224. package/dist/src/tools/conventions.js.map +0 -1
  225. package/dist/src/tools/decisions.d.ts +0 -3
  226. package/dist/src/tools/decisions.d.ts.map +0 -1
  227. package/dist/src/tools/decisions.js +0 -136
  228. package/dist/src/tools/decisions.js.map +0 -1
  229. package/dist/src/tools/export-import.d.ts +0 -3
  230. package/dist/src/tools/export-import.d.ts.map +0 -1
  231. package/dist/src/tools/export-import.js +0 -180
  232. package/dist/src/tools/export-import.js.map +0 -1
  233. package/dist/src/tools/file-notes.d.ts +0 -3
  234. package/dist/src/tools/file-notes.d.ts.map +0 -1
  235. package/dist/src/tools/file-notes.js +0 -104
  236. package/dist/src/tools/file-notes.js.map +0 -1
  237. package/dist/src/tools/intelligence.d.ts +0 -3
  238. package/dist/src/tools/intelligence.d.ts.map +0 -1
  239. package/dist/src/tools/intelligence.js +0 -427
  240. package/dist/src/tools/intelligence.js.map +0 -1
  241. package/dist/src/tools/milestones.d.ts +0 -3
  242. package/dist/src/tools/milestones.d.ts.map +0 -1
  243. package/dist/src/tools/milestones.js +0 -71
  244. package/dist/src/tools/milestones.js.map +0 -1
  245. package/dist/src/tools/scheduler.d.ts +0 -3
  246. package/dist/src/tools/scheduler.d.ts.map +0 -1
  247. package/dist/src/tools/scheduler.js +0 -363
  248. package/dist/src/tools/scheduler.js.map +0 -1
  249. package/dist/src/tools/sessions.d.ts +0 -3
  250. package/dist/src/tools/sessions.d.ts.map +0 -1
  251. package/dist/src/tools/sessions.js +0 -356
  252. package/dist/src/tools/sessions.js.map +0 -1
  253. package/dist/src/tools/stats.d.ts +0 -3
  254. package/dist/src/tools/stats.d.ts.map +0 -1
  255. package/dist/src/tools/stats.js +0 -63
  256. package/dist/src/tools/stats.js.map +0 -1
  257. package/dist/src/tools/tasks.d.ts +0 -3
  258. package/dist/src/tools/tasks.d.ts.map +0 -1
  259. package/dist/src/tools/tasks.js +0 -206
  260. package/dist/src/tools/tasks.js.map +0 -1
  261. package/dist/src/types.d.ts +0 -170
  262. package/dist/src/types.d.ts.map +0 -1
  263. package/dist/src/types.js +0 -5
  264. package/dist/src/types.js.map +0 -1
  265. package/dist/src/utils.d.ts +0 -58
  266. package/dist/src/utils.d.ts.map +0 -1
  267. package/dist/src/utils.js +0 -190
  268. package/dist/src/utils.js.map +0 -1
  269. package/dist/tests/helpers/test-db.d.ts +0 -12
  270. package/dist/tests/helpers/test-db.d.ts.map +0 -1
  271. package/dist/tests/helpers/test-db.js +0 -149
  272. package/dist/tests/helpers/test-db.js.map +0 -1
  273. package/dist/tests/installer/installer.test.d.ts +0 -2
  274. package/dist/tests/installer/installer.test.d.ts.map +0 -1
  275. package/dist/tests/installer/installer.test.js +0 -160
  276. package/dist/tests/installer/installer.test.js.map +0 -1
  277. package/dist/tests/repositories/repos.test.d.ts +0 -2
  278. package/dist/tests/repositories/repos.test.d.ts.map +0 -1
  279. package/dist/tests/repositories/repos.test.js +0 -220
  280. package/dist/tests/repositories/repos.test.js.map +0 -1
  281. package/dist/tools/maintenance.d.ts +0 -3
  282. package/dist/tools/maintenance.d.ts.map +0 -1
  283. package/dist/tools/maintenance.js +0 -647
  284. package/dist/tools/maintenance.js.map +0 -1
  285. package/dist/tools/memory.d.ts +0 -3
  286. package/dist/tools/memory.d.ts.map +0 -1
  287. package/dist/tools/memory.js +0 -446
  288. package/dist/tools/memory.js.map +0 -1
@@ -1,647 +0,0 @@
1
- // ============================================================================
2
- // Engram MCP Server — Maintenance, Backup & Milestone Tools
3
- // ============================================================================
4
- import { z } from "zod";
5
- import * as fs from "fs";
6
- import * as path from "path";
7
- import { getDb, now, getCurrentSessionId, getProjectRoot, getDbSizeKb, getDbPath, backupDatabase } from "../database.js";
8
- import { TOOL_PREFIX, DB_DIR_NAME, BACKUP_DIR_NAME, COMPACTION_THRESHOLD_SESSIONS, MAX_BACKUP_COUNT, SERVER_VERSION } from "../constants.js";
9
- import { log } from "../logger.js";
10
- export function registerMaintenanceTools(server) {
11
- // ─── MEMORY STATS ───────────────────────────────────────────────────
12
- server.registerTool(`${TOOL_PREFIX}_stats`, {
13
- title: "Memory Statistics",
14
- description: `Get a comprehensive overview of everything stored in Engram's memory: session count, changes, decisions, file notes, conventions, tasks, milestones, most-changed files, and database size.
15
-
16
- Returns:
17
- MemoryStats object with counts and insights.`,
18
- inputSchema: {},
19
- annotations: {
20
- readOnlyHint: true,
21
- destructiveHint: false,
22
- idempotentHint: true,
23
- openWorldHint: false,
24
- },
25
- }, async () => {
26
- const db = getDb();
27
- const count = (table) => db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get().c;
28
- const oldest = db.prepare("SELECT started_at FROM sessions ORDER BY id ASC LIMIT 1").get();
29
- const newest = db.prepare("SELECT started_at FROM sessions ORDER BY id DESC LIMIT 1").get();
30
- const mostChanged = db.prepare(`
31
- SELECT file_path, COUNT(*) as change_count
32
- FROM changes GROUP BY file_path ORDER BY change_count DESC LIMIT 10
33
- `).all();
34
- // Layer distribution from file notes
35
- const layerDist = db.prepare(`
36
- SELECT layer, COUNT(*) as count FROM file_notes WHERE layer IS NOT NULL GROUP BY layer ORDER BY count DESC
37
- `).all();
38
- // Task stats
39
- const tasksByStatus = db.prepare(`
40
- SELECT status, COUNT(*) as count FROM tasks GROUP BY status ORDER BY count DESC
41
- `).all();
42
- // Schema version
43
- let schemaVersion = 0;
44
- try {
45
- const vRow = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'").get();
46
- schemaVersion = vRow ? parseInt(vRow.value, 10) : 0;
47
- }
48
- catch { /* no schema_meta */ }
49
- const stats = {
50
- total_sessions: count("sessions"),
51
- total_changes: count("changes"),
52
- total_decisions: count("decisions"),
53
- total_file_notes: count("file_notes"),
54
- total_conventions: count("conventions"),
55
- total_tasks: count("tasks"),
56
- total_milestones: count("milestones"),
57
- oldest_session: oldest?.started_at || null,
58
- newest_session: newest?.started_at || null,
59
- most_changed_files: mostChanged,
60
- database_size_kb: getDbSizeKb(),
61
- layer_distribution: layerDist,
62
- tasks_by_status: tasksByStatus,
63
- schema_version: schemaVersion,
64
- engine: "better-sqlite3 (WAL mode)",
65
- };
66
- return {
67
- content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
68
- };
69
- });
70
- // ─── COMPACT MEMORY ─────────────────────────────────────────────────
71
- server.registerTool(`${TOOL_PREFIX}_compact`, {
72
- title: "Compact Memory",
73
- description: `Compact old session data to reduce database size. Merges change records from old sessions into summaries and removes granular entries. Sessions newer than the threshold are preserved in full. Automatically creates a backup before compacting.
74
-
75
- Args:
76
- - keep_sessions (number, optional): Number of recent sessions to keep in full detail (default: ${COMPACTION_THRESHOLD_SESSIONS})
77
- - max_age_days (number, optional): Also remove sessions older than N days (default: no age limit)
78
- - dry_run (boolean, optional): Show what would be compacted without actually doing it (default: true)
79
-
80
- Returns:
81
- CompactionResult with counts and freed storage.`,
82
- inputSchema: {
83
- keep_sessions: z.number().int().min(5).default(COMPACTION_THRESHOLD_SESSIONS).describe("Recent sessions to preserve"),
84
- max_age_days: z.number().int().min(7).optional().describe("Remove sessions older than N days"),
85
- dry_run: z.boolean().default(true).describe("Preview mode — no changes made"),
86
- },
87
- annotations: {
88
- readOnlyHint: false,
89
- destructiveHint: true,
90
- idempotentHint: false,
91
- openWorldHint: false,
92
- },
93
- }, async ({ keep_sessions, max_age_days, dry_run }) => {
94
- const db = getDb();
95
- // Find the cutoff session ID
96
- const cutoff = db.prepare("SELECT id FROM sessions ORDER BY id DESC LIMIT 1 OFFSET ?").get(keep_sessions);
97
- if (!cutoff) {
98
- return {
99
- content: [{ type: "text", text: "Not enough sessions to compact. Nothing to do." }],
100
- };
101
- }
102
- // Count what would be compacted
103
- let sessionsQuery = "SELECT COUNT(*) as c FROM sessions WHERE id <= ? AND ended_at IS NOT NULL";
104
- const sessionsParams = [cutoff.id];
105
- if (max_age_days) {
106
- const cutoffDate = new Date(Date.now() - max_age_days * 86400000).toISOString();
107
- sessionsQuery += " AND started_at < ?";
108
- sessionsParams.push(cutoffDate);
109
- }
110
- const sessionsToCompact = db.prepare(sessionsQuery).get(...sessionsParams).c;
111
- const changesToSummarize = db.prepare("SELECT COUNT(*) as c FROM changes WHERE session_id <= ?").get(cutoff.id).c;
112
- const sizeBefore = getDbSizeKb();
113
- if (dry_run) {
114
- return {
115
- content: [{
116
- type: "text",
117
- text: JSON.stringify({
118
- dry_run: true,
119
- would_compact: {
120
- sessions: sessionsToCompact,
121
- changes: changesToSummarize,
122
- },
123
- message: `Would compact ${sessionsToCompact} sessions and summarize ${changesToSummarize} change records. Run with dry_run=false to execute.`,
124
- }, null, 2),
125
- }],
126
- };
127
- }
128
- // ─── Auto-backup before compacting ──────────────────────────
129
- let backupPath = "";
130
- try {
131
- backupPath = backupDatabase();
132
- log.info(`Auto-backup created before compaction: ${backupPath}`);
133
- }
134
- catch (e) {
135
- log.warn(`Failed to create backup before compaction: ${e}`);
136
- }
137
- // Execute compaction in a transaction
138
- const compact = db.transaction(() => {
139
- // For each old session, create a summarized change record
140
- const oldSessions = db.prepare("SELECT id, summary FROM sessions WHERE id <= ? AND ended_at IS NOT NULL").all(cutoff.id);
141
- for (const session of oldSessions) {
142
- const changes = db.prepare("SELECT change_type, file_path, description FROM changes WHERE session_id = ?").all(session.id);
143
- if (changes.length > 0) {
144
- // Create one summary change per old session
145
- const summaryDesc = changes.map(c => `[${c.change_type}] ${c.file_path}: ${c.description}`).join("; ");
146
- db.prepare("INSERT INTO changes (session_id, timestamp, file_path, change_type, description, impact_scope) VALUES (?, ?, ?, ?, ?, ?)").run(session.id, now(), "(compacted)", "modified", `Compacted ${changes.length} changes: ${summaryDesc.slice(0, 2000)}`, "global");
147
- }
148
- // Delete granular changes
149
- db.prepare("DELETE FROM changes WHERE session_id = ? AND file_path != '(compacted)'").run(session.id);
150
- }
151
- });
152
- compact();
153
- // Vacuum to reclaim space (must be outside transaction)
154
- db.exec("VACUUM");
155
- const sizeAfter = getDbSizeKb();
156
- const result = {
157
- sessions_compacted: sessionsToCompact,
158
- changes_summarized: changesToSummarize,
159
- storage_freed_kb: Math.max(0, sizeBefore - sizeAfter),
160
- };
161
- return {
162
- content: [{
163
- type: "text",
164
- text: JSON.stringify({
165
- ...result,
166
- backup_path: backupPath || null,
167
- message: `Compaction complete. ${backupPath ? `Backup saved at ${backupPath}.` : ""}`,
168
- }, null, 2),
169
- }],
170
- };
171
- });
172
- // ─── BACKUP DATABASE ───────────────────────────────────────────────
173
- server.registerTool(`${TOOL_PREFIX}_backup`, {
174
- title: "Backup Database",
175
- description: `Create a backup of the Engram memory database. Uses SQLite's native backup API for safe, consistent copies. Save to any path — including cloud-synced folders (Dropbox, OneDrive, Google Drive) for cross-machine portability.
176
-
177
- Args:
178
- - output_path (string, optional): Where to save the backup (default: .engram/backups/memory-{timestamp}.db)
179
- - prune_old (boolean, optional): Remove old backups beyond the max count (default: true)
180
-
181
- Returns:
182
- BackupInfo with path, size, and timestamp.`,
183
- inputSchema: {
184
- output_path: z.string().optional().describe("Custom backup destination path"),
185
- prune_old: z.boolean().default(true).describe("Prune old backups beyond the max count"),
186
- },
187
- annotations: {
188
- readOnlyHint: false,
189
- destructiveHint: false,
190
- idempotentHint: false,
191
- openWorldHint: true,
192
- },
193
- }, async ({ output_path, prune_old }) => {
194
- const backupPath = backupDatabase(output_path);
195
- const stats = fs.statSync(backupPath);
196
- const sizeKb = Math.round(stats.size / 1024);
197
- // Get schema version
198
- let dbVersion = 0;
199
- try {
200
- const db = getDb();
201
- const vRow = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'").get();
202
- dbVersion = vRow ? parseInt(vRow.value, 10) : 0;
203
- }
204
- catch { /* skip */ }
205
- const info = {
206
- path: backupPath,
207
- size_kb: sizeKb,
208
- created_at: now(),
209
- database_version: dbVersion,
210
- };
211
- // Prune old backups if saving to default directory
212
- if (prune_old && !output_path) {
213
- const backupDir = path.join(getProjectRoot(), DB_DIR_NAME, BACKUP_DIR_NAME);
214
- try {
215
- const files = fs.readdirSync(backupDir)
216
- .filter(f => f.startsWith("memory-") && f.endsWith(".db"))
217
- .map(f => ({
218
- name: f,
219
- path: path.join(backupDir, f),
220
- mtime: fs.statSync(path.join(backupDir, f)).mtimeMs,
221
- }))
222
- .sort((a, b) => b.mtime - a.mtime);
223
- if (files.length > MAX_BACKUP_COUNT) {
224
- const toDelete = files.slice(MAX_BACKUP_COUNT);
225
- for (const f of toDelete) {
226
- fs.unlinkSync(f.path);
227
- }
228
- info.pruned = toDelete.length;
229
- }
230
- }
231
- catch { /* skip pruning */ }
232
- }
233
- return {
234
- content: [{
235
- type: "text",
236
- text: JSON.stringify({
237
- ...info,
238
- message: `Backup created successfully at ${backupPath} (${sizeKb} KB).`,
239
- }, null, 2),
240
- }],
241
- };
242
- });
243
- // ─── RESTORE DATABASE ──────────────────────────────────────────────
244
- server.registerTool(`${TOOL_PREFIX}_restore`, {
245
- title: "Restore Database",
246
- description: `Restore the Engram memory database from a backup file. Creates a safety backup of the current database before overwriting. The MCP server will need to be restarted after restore.
247
-
248
- Args:
249
- - input_path (string): Path to the backup .db file
250
- - confirm (string): Must be "yes-restore" to execute
251
-
252
- Returns:
253
- Confirmation and instructions to restart.`,
254
- inputSchema: {
255
- input_path: z.string().describe("Path to the backup .db file"),
256
- confirm: z.string().describe('Type "yes-restore" to confirm'),
257
- },
258
- annotations: {
259
- readOnlyHint: false,
260
- destructiveHint: true,
261
- idempotentHint: false,
262
- openWorldHint: true,
263
- },
264
- }, async ({ input_path, confirm }) => {
265
- if (confirm !== "yes-restore") {
266
- return {
267
- isError: true,
268
- content: [{ type: "text", text: 'Safety check: set confirm to "yes-restore" to proceed.' }],
269
- };
270
- }
271
- const projectRoot = getProjectRoot();
272
- const inputPath = path.isAbsolute(input_path) ? input_path : path.join(projectRoot, input_path);
273
- if (!fs.existsSync(inputPath)) {
274
- return { isError: true, content: [{ type: "text", text: `Backup file not found: ${inputPath}` }] };
275
- }
276
- // Create safety backup of current database
277
- let safetyBackupPath = "";
278
- try {
279
- safetyBackupPath = backupDatabase();
280
- log.info(`Safety backup created before restore: ${safetyBackupPath}`);
281
- }
282
- catch (e) {
283
- return {
284
- isError: true,
285
- content: [{ type: "text", text: `Failed to create safety backup before restore: ${e}. Aborting.` }],
286
- };
287
- }
288
- // Copy the backup file over the current database
289
- const dbPath = getDbPath();
290
- try {
291
- fs.copyFileSync(inputPath, dbPath);
292
- }
293
- catch (e) {
294
- return {
295
- isError: true,
296
- content: [{ type: "text", text: `Failed to restore: ${e}. Your previous database is backed up at ${safetyBackupPath}.` }],
297
- };
298
- }
299
- return {
300
- content: [{
301
- type: "text",
302
- text: JSON.stringify({
303
- restored_from: inputPath,
304
- safety_backup: safetyBackupPath,
305
- message: "Database restored successfully. Please RESTART the MCP server to load the restored database. A safety backup of the previous database was created.",
306
- }, null, 2),
307
- }],
308
- };
309
- });
310
- // ─── LIST BACKUPS ──────────────────────────────────────────────────
311
- server.registerTool(`${TOOL_PREFIX}_list_backups`, {
312
- title: "List Backups",
313
- description: `List all available backup files in the default backup directory.
314
-
315
- Returns:
316
- Array of backup files with sizes and timestamps.`,
317
- inputSchema: {},
318
- annotations: {
319
- readOnlyHint: true,
320
- destructiveHint: false,
321
- idempotentHint: true,
322
- openWorldHint: false,
323
- },
324
- }, async () => {
325
- const backupDir = path.join(getProjectRoot(), DB_DIR_NAME, BACKUP_DIR_NAME);
326
- if (!fs.existsSync(backupDir)) {
327
- return {
328
- content: [{ type: "text", text: JSON.stringify({ backups: [], message: "No backups found." }, null, 2) }],
329
- };
330
- }
331
- const files = fs.readdirSync(backupDir)
332
- .filter(f => f.endsWith(".db"))
333
- .map(f => {
334
- const fullPath = path.join(backupDir, f);
335
- const stats = fs.statSync(fullPath);
336
- return {
337
- filename: f,
338
- path: fullPath,
339
- size_kb: Math.round(stats.size / 1024),
340
- created_at: stats.mtime.toISOString(),
341
- };
342
- })
343
- .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
344
- return {
345
- content: [{
346
- type: "text",
347
- text: JSON.stringify({
348
- backup_directory: backupDir,
349
- count: files.length,
350
- backups: files,
351
- }, null, 2),
352
- }],
353
- };
354
- });
355
- // ─── RECORD MILESTONE ───────────────────────────────────────────────
356
- server.registerTool(`${TOOL_PREFIX}_record_milestone`, {
357
- title: "Record Milestone",
358
- description: `Record a major project milestone or achievement. Milestones mark significant points in the project timeline — feature completions, releases, major refactors, etc.
359
-
360
- Args:
361
- - title (string): Milestone title
362
- - description (string, optional): What was achieved
363
- - version (string, optional): Version number if applicable
364
- - tags (array, optional): Tags
365
-
366
- Returns:
367
- Milestone ID and confirmation.`,
368
- inputSchema: {
369
- title: z.string().min(3).describe("Milestone title"),
370
- description: z.string().optional().describe("What was achieved"),
371
- version: z.string().optional().describe("Version number"),
372
- tags: z.array(z.string()).optional(),
373
- },
374
- annotations: {
375
- readOnlyHint: false,
376
- destructiveHint: false,
377
- idempotentHint: false,
378
- openWorldHint: false,
379
- },
380
- }, async ({ title, description, version, tags }) => {
381
- const db = getDb();
382
- const timestamp = now();
383
- const sessionId = getCurrentSessionId();
384
- const result = db.prepare("INSERT INTO milestones (session_id, timestamp, title, description, version, tags) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, timestamp, title, description || null, version || null, tags ? JSON.stringify(tags) : null);
385
- return {
386
- content: [{
387
- type: "text",
388
- text: JSON.stringify({
389
- milestone_id: result.lastInsertRowid,
390
- message: `Milestone #${result.lastInsertRowid} recorded: "${title}"${version ? ` (v${version})` : ""}`,
391
- }, null, 2),
392
- }],
393
- };
394
- });
395
- server.registerTool(`${TOOL_PREFIX}_get_milestones`, {
396
- title: "Get Milestones",
397
- description: `Retrieve project milestones. Shows the project's achievement timeline.
398
-
399
- Args:
400
- - limit (number, optional): Max results (default 20)
401
-
402
- Returns:
403
- Array of milestones in reverse chronological order.`,
404
- inputSchema: {
405
- limit: z.number().int().min(1).max(100).default(20),
406
- },
407
- annotations: {
408
- readOnlyHint: true,
409
- destructiveHint: false,
410
- idempotentHint: true,
411
- openWorldHint: false,
412
- },
413
- }, async ({ limit }) => {
414
- const db = getDb();
415
- const milestones = db.prepare("SELECT * FROM milestones ORDER BY timestamp DESC LIMIT ?").all(limit);
416
- return { content: [{ type: "text", text: JSON.stringify({ milestones }, null, 2) }] };
417
- });
418
- // ─── EXPORT MEMORY ──────────────────────────────────────────────────
419
- server.registerTool(`${TOOL_PREFIX}_export`, {
420
- title: "Export Memory",
421
- description: `Export the entire memory database as a portable JSON file. Useful for backup, migration, or sharing project knowledge with teammates.
422
-
423
- Args:
424
- - output_path (string, optional): Where to save the export (default: .engram/export.json)
425
-
426
- Returns:
427
- Export file path and summary.`,
428
- inputSchema: {
429
- output_path: z.string().optional().describe("Export file path"),
430
- },
431
- annotations: {
432
- readOnlyHint: true,
433
- destructiveHint: false,
434
- idempotentHint: true,
435
- openWorldHint: true,
436
- },
437
- }, async ({ output_path }) => {
438
- const db = getDb();
439
- const projectRoot = getProjectRoot();
440
- const exportData = {
441
- engram_version: SERVER_VERSION,
442
- exported_at: now(),
443
- project_root: projectRoot,
444
- sessions: db.prepare("SELECT * FROM sessions ORDER BY id").all(),
445
- changes: db.prepare("SELECT * FROM changes ORDER BY id").all(),
446
- decisions: db.prepare("SELECT * FROM decisions ORDER BY id").all(),
447
- file_notes: db.prepare("SELECT * FROM file_notes ORDER BY file_path").all(),
448
- conventions: db.prepare("SELECT * FROM conventions ORDER BY id").all(),
449
- tasks: db.prepare("SELECT * FROM tasks ORDER BY id").all(),
450
- milestones: db.prepare("SELECT * FROM milestones ORDER BY id").all(),
451
- };
452
- const filePath = output_path || path.join(projectRoot, DB_DIR_NAME, "export.json");
453
- fs.writeFileSync(filePath, JSON.stringify(exportData, null, 2));
454
- return {
455
- content: [{
456
- type: "text",
457
- text: JSON.stringify({
458
- exported_to: filePath,
459
- counts: {
460
- sessions: exportData.sessions.length,
461
- changes: exportData.changes.length,
462
- decisions: exportData.decisions.length,
463
- file_notes: exportData.file_notes.length,
464
- conventions: exportData.conventions.length,
465
- tasks: exportData.tasks.length,
466
- milestones: exportData.milestones.length,
467
- },
468
- }, null, 2),
469
- }],
470
- };
471
- });
472
- // ─── IMPORT MEMORY ──────────────────────────────────────────────────
473
- server.registerTool(`${TOOL_PREFIX}_import`, {
474
- title: "Import Memory",
475
- description: `Import memory from a previously exported JSON file. Merges data into the existing database without duplicating existing records.
476
-
477
- Args:
478
- - input_path (string): Path to the export JSON file
479
- - dry_run (boolean, optional): Preview import without writing (default: true)
480
-
481
- Returns:
482
- Import summary with counts.`,
483
- inputSchema: {
484
- input_path: z.string().describe("Path to export JSON file"),
485
- dry_run: z.boolean().default(true).describe("Preview mode"),
486
- },
487
- annotations: {
488
- readOnlyHint: false,
489
- destructiveHint: false,
490
- idempotentHint: true,
491
- openWorldHint: true,
492
- },
493
- }, async ({ input_path, dry_run }) => {
494
- const projectRoot = getProjectRoot();
495
- const filePath = path.isAbsolute(input_path) ? input_path : path.join(projectRoot, input_path);
496
- if (!fs.existsSync(filePath)) {
497
- return { isError: true, content: [{ type: "text", text: `File not found: ${filePath}` }] };
498
- }
499
- let importData;
500
- try {
501
- importData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
502
- }
503
- catch (e) {
504
- return { isError: true, content: [{ type: "text", text: `Invalid JSON: ${e}` }] };
505
- }
506
- const counts = {};
507
- for (const key of ["sessions", "changes", "decisions", "file_notes", "conventions", "tasks", "milestones"]) {
508
- counts[key] = Array.isArray(importData[key]) ? importData[key].length : 0;
509
- }
510
- if (dry_run) {
511
- return {
512
- content: [{
513
- type: "text",
514
- text: JSON.stringify({
515
- dry_run: true,
516
- would_import: counts,
517
- message: "Run with dry_run=false to execute the import.",
518
- }, null, 2),
519
- }],
520
- };
521
- }
522
- // Actual import — decisions, conventions, file_notes, tasks, milestones
523
- const db = getDb();
524
- const importTransaction = db.transaction(() => {
525
- // ─── Import sessions (skip duplicates by started_at + agent_name) ───
526
- const sessionIdMap = new Map(); // old → new
527
- if (Array.isArray(importData.sessions)) {
528
- for (const s of importData.sessions) {
529
- const exists = db.prepare("SELECT id FROM sessions WHERE started_at = ? AND agent_name = ?").get(s.started_at, s.agent_name || "unknown");
530
- if (!exists) {
531
- const result = db.prepare("INSERT INTO sessions (started_at, ended_at, summary, agent_name, project_root, tags) VALUES (?, ?, ?, ?, ?, ?)").run(s.started_at, s.ended_at || null, s.summary || null, s.agent_name || "unknown", s.project_root || "", s.tags || null);
532
- sessionIdMap.set(s.id, result.lastInsertRowid);
533
- }
534
- else {
535
- sessionIdMap.set(s.id, exists.id);
536
- }
537
- }
538
- }
539
- // ─── Import changes (skip duplicates by file_path + timestamp + description) ───
540
- if (Array.isArray(importData.changes)) {
541
- for (const c of importData.changes) {
542
- const exists = db.prepare("SELECT id FROM changes WHERE file_path = ? AND timestamp = ? AND description = ?").get(c.file_path, c.timestamp, c.description);
543
- if (!exists) {
544
- // Map old session_id to new session_id
545
- const mappedSessionId = c.session_id ? (sessionIdMap.get(c.session_id) ?? c.session_id) : null;
546
- db.prepare("INSERT INTO changes (session_id, timestamp, file_path, change_type, description, diff_summary, impact_scope) VALUES (?, ?, ?, ?, ?, ?, ?)").run(mappedSessionId, c.timestamp, c.file_path, c.change_type, c.description, c.diff_summary || null, c.impact_scope || "local");
547
- }
548
- }
549
- }
550
- // Import conventions (skip duplicates by rule text)
551
- if (Array.isArray(importData.conventions)) {
552
- for (const c of importData.conventions) {
553
- const exists = db.prepare("SELECT id FROM conventions WHERE rule = ?").get(c.rule);
554
- if (!exists) {
555
- db.prepare("INSERT INTO conventions (timestamp, category, rule, examples, enforced) VALUES (?, ?, ?, ?, ?)").run(c.timestamp || now(), c.category, c.rule, c.examples || null, c.enforced ?? 1);
556
- }
557
- }
558
- }
559
- // Import decisions (skip duplicates by decision text)
560
- if (Array.isArray(importData.decisions)) {
561
- for (const d of importData.decisions) {
562
- const exists = db.prepare("SELECT id FROM decisions WHERE decision = ?").get(d.decision);
563
- if (!exists) {
564
- db.prepare("INSERT INTO decisions (timestamp, decision, rationale, affected_files, tags, status) VALUES (?, ?, ?, ?, ?, ?)").run(d.timestamp || now(), d.decision, d.rationale || null, d.affected_files || null, d.tags || null, d.status || "active");
565
- }
566
- }
567
- }
568
- // Import file notes (upsert)
569
- if (Array.isArray(importData.file_notes)) {
570
- for (const f of importData.file_notes) {
571
- db.prepare(`
572
- INSERT OR REPLACE INTO file_notes (file_path, purpose, dependencies, dependents, layer, last_reviewed, notes, complexity)
573
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
574
- `).run(f.file_path, f.purpose || null, f.dependencies || null, f.dependents || null, f.layer || null, f.last_reviewed || now(), f.notes || null, f.complexity || null);
575
- }
576
- }
577
- // Import milestones (skip duplicates by title + timestamp)
578
- if (Array.isArray(importData.milestones)) {
579
- for (const m of importData.milestones) {
580
- const exists = db.prepare("SELECT id FROM milestones WHERE title = ? AND timestamp = ?").get(m.title, m.timestamp);
581
- if (!exists) {
582
- db.prepare("INSERT INTO milestones (timestamp, title, description, version, tags) VALUES (?, ?, ?, ?, ?)").run(m.timestamp || now(), m.title, m.description || null, m.version || null, m.tags || null);
583
- }
584
- }
585
- }
586
- });
587
- importTransaction();
588
- return {
589
- content: [{
590
- type: "text",
591
- text: JSON.stringify({ imported: counts, message: "Import complete. Duplicates were skipped." }, null, 2),
592
- }],
593
- };
594
- });
595
- // ─── CLEAR MEMORY ───────────────────────────────────────────────────
596
- server.registerTool(`${TOOL_PREFIX}_clear`, {
597
- title: "Clear Memory",
598
- description: `Clear specific tables or the entire memory database. USE WITH EXTREME CAUTION. This is irreversible. A backup is automatically created before clearing.
599
-
600
- Args:
601
- - scope: "all" | "sessions" | "changes" | "decisions" | "file_notes" | "conventions" | "tasks" | "milestones" | "cache"
602
- - confirm (string): Must be "yes-delete-permanently" to execute
603
-
604
- Returns:
605
- Confirmation of what was cleared.`,
606
- inputSchema: {
607
- scope: z.enum(["all", "sessions", "changes", "decisions", "file_notes", "conventions", "tasks", "milestones", "cache"]),
608
- confirm: z.string().describe('Type "yes-delete-permanently" to confirm'),
609
- },
610
- annotations: {
611
- readOnlyHint: false,
612
- destructiveHint: true,
613
- idempotentHint: true,
614
- openWorldHint: false,
615
- },
616
- }, async ({ scope, confirm }) => {
617
- if (confirm !== "yes-delete-permanently") {
618
- return {
619
- isError: true,
620
- content: [{ type: "text", text: 'Safety check: set confirm to "yes-delete-permanently" to proceed.' }],
621
- };
622
- }
623
- // ─── Auto-backup before clearing ──────────────────────────
624
- let backupPath = "";
625
- try {
626
- backupPath = backupDatabase();
627
- log.info(`Auto-backup created before clear: ${backupPath}`);
628
- }
629
- catch (e) {
630
- log.warn(`Failed to create backup before clear: ${e}`);
631
- }
632
- const db = getDb();
633
- const tables = scope === "all"
634
- ? ["sessions", "changes", "decisions", "file_notes", "conventions", "tasks", "milestones", "snapshot_cache"]
635
- : scope === "cache" ? ["snapshot_cache"] : [scope];
636
- for (const table of tables) {
637
- db.prepare(`DELETE FROM ${table}`).run();
638
- }
639
- return {
640
- content: [{
641
- type: "text",
642
- text: `Cleared: ${tables.join(", ")}. Memory has been reset.${backupPath ? ` Backup saved at ${backupPath}.` : ""}`,
643
- }],
644
- };
645
- });
646
- }
647
- //# sourceMappingURL=maintenance.js.map