engram-mcp-server 1.2.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 (44) hide show
  1. package/README.md +645 -0
  2. package/dist/constants.d.ts +21 -0
  3. package/dist/constants.js +81 -0
  4. package/dist/database.d.ts +30 -0
  5. package/dist/database.js +134 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.js +67 -0
  8. package/dist/migrations.d.ts +4 -0
  9. package/dist/migrations.js +342 -0
  10. package/dist/scripts/install-hooks.d.ts +3 -0
  11. package/dist/scripts/install-hooks.js +89 -0
  12. package/dist/tools/intelligence.d.ts +3 -0
  13. package/dist/tools/intelligence.js +427 -0
  14. package/dist/tools/maintenance.d.ts +3 -0
  15. package/dist/tools/maintenance.js +646 -0
  16. package/dist/tools/memory.d.ts +3 -0
  17. package/dist/tools/memory.js +446 -0
  18. package/dist/tools/scheduler.d.ts +3 -0
  19. package/dist/tools/scheduler.js +363 -0
  20. package/dist/tools/sessions.d.ts +3 -0
  21. package/dist/tools/sessions.js +355 -0
  22. package/dist/tools/tasks.d.ts +3 -0
  23. package/dist/tools/tasks.js +206 -0
  24. package/dist/types.d.ts +170 -0
  25. package/dist/types.js +5 -0
  26. package/dist/utils.d.ts +58 -0
  27. package/dist/utils.js +190 -0
  28. package/docs/scheduled-events.md +150 -0
  29. package/package.json +43 -0
  30. package/scripts/install-mcp.js +175 -0
  31. package/src/constants.ts +86 -0
  32. package/src/database.ts +162 -0
  33. package/src/index.ts +79 -0
  34. package/src/migrations.ts +367 -0
  35. package/src/scripts/install-hooks.ts +96 -0
  36. package/src/tools/intelligence.ts +469 -0
  37. package/src/tools/maintenance.ts +783 -0
  38. package/src/tools/memory.ts +543 -0
  39. package/src/tools/scheduler.ts +413 -0
  40. package/src/tools/sessions.ts +430 -0
  41. package/src/tools/tasks.ts +215 -0
  42. package/src/types.ts +267 -0
  43. package/src/utils.ts +216 -0
  44. package/tsconfig.json +19 -0
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================================
3
+ // Engram — Git Hook Installer
4
+ //
5
+ // Installs a post-commit hook that records git commits in Engram's memory,
6
+ // so the agent always knows what changed between sessions even without
7
+ // explicitly recording changes.
8
+ // ============================================================================
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ const HOOK_CONTENT = `#!/bin/bash
12
+ # ─────────────────────────────────────────────────────────────
13
+ # Engram Post-Commit Hook
14
+ # Records commit info into .engram/git-changes.log
15
+ # The Engram MCP server reads this on session start.
16
+ # ─────────────────────────────────────────────────────────────
17
+
18
+ ENGRAM_DIR=".engram"
19
+ CHANGE_LOG="$ENGRAM_DIR/git-changes.log"
20
+
21
+ # Ensure the directory exists
22
+ mkdir -p "$ENGRAM_DIR"
23
+
24
+ # Get commit info
25
+ HASH=$(git rev-parse --short HEAD)
26
+ MSG=$(git log -1 --pretty=format:"%s")
27
+ AUTHOR=$(git log -1 --pretty=format:"%an")
28
+ DATE=$(git log -1 --pretty=format:"%aI")
29
+ FILES=$(git diff-tree --no-commit-id --name-status -r HEAD)
30
+
31
+ # Append to change log
32
+ {
33
+ echo "--- COMMIT $HASH ---"
34
+ echo "date: $DATE"
35
+ echo "author: $AUTHOR"
36
+ echo "message: $MSG"
37
+ echo "files:"
38
+ echo "$FILES"
39
+ echo "---"
40
+ echo ""
41
+ } >> "$CHANGE_LOG"
42
+
43
+ # Keep only last 200 entries to prevent unbounded growth
44
+ if [ -f "$CHANGE_LOG" ]; then
45
+ LINES=$(wc -l < "$CHANGE_LOG")
46
+ if [ "$LINES" -gt 2000 ]; then
47
+ tail -1000 "$CHANGE_LOG" > "$CHANGE_LOG.tmp"
48
+ mv "$CHANGE_LOG.tmp" "$CHANGE_LOG"
49
+ fi
50
+ fi
51
+ `;
52
+ function installHooks() {
53
+ // Find .git directory
54
+ let dir = process.cwd();
55
+ while (dir !== path.dirname(dir)) {
56
+ if (fs.existsSync(path.join(dir, ".git"))) {
57
+ break;
58
+ }
59
+ dir = path.dirname(dir);
60
+ }
61
+ const gitDir = path.join(dir, ".git");
62
+ if (!fs.existsSync(gitDir)) {
63
+ console.error("Error: Not a git repository. Navigate to a project with .git and try again.");
64
+ process.exit(1);
65
+ }
66
+ const hooksDir = path.join(gitDir, "hooks");
67
+ fs.mkdirSync(hooksDir, { recursive: true });
68
+ const hookPath = path.join(hooksDir, "post-commit");
69
+ // Check for existing hook
70
+ if (fs.existsSync(hookPath)) {
71
+ const existing = fs.readFileSync(hookPath, "utf-8");
72
+ if (existing.includes("Engram Post-Commit Hook")) {
73
+ console.log("Engram post-commit hook is already installed.");
74
+ return;
75
+ }
76
+ // Append to existing hook
77
+ fs.appendFileSync(hookPath, "\n\n" + HOOK_CONTENT);
78
+ console.log("Engram post-commit hook appended to existing hook.");
79
+ }
80
+ else {
81
+ fs.writeFileSync(hookPath, HOOK_CONTENT);
82
+ console.log("Engram post-commit hook installed.");
83
+ }
84
+ // Make executable
85
+ fs.chmodSync(hookPath, "755");
86
+ console.log(`Hook location: ${hookPath}`);
87
+ }
88
+ installHooks();
89
+ //# sourceMappingURL=install-hooks.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerIntelligenceTools(server: McpServer): void;
3
+ //# sourceMappingURL=intelligence.d.ts.map
@@ -0,0 +1,427 @@
1
+ // ============================================================================
2
+ // Engram MCP Server — Project Intelligence Tools
3
+ // ============================================================================
4
+ import { z } from "zod";
5
+ import { getDb, now, getProjectRoot } from "../database.js";
6
+ import { TOOL_PREFIX, SNAPSHOT_TTL_MINUTES, MAX_SEARCH_RESULTS } from "../constants.js";
7
+ import { scanFileTree, detectLayer, isGitRepo, getGitBranch, getGitHead, getGitLogSince, getGitFilesChanged, minutesSince, safeJsonParse } from "../utils.js";
8
+ // ─── FTS5 Helpers ────────────────────────────────────────────────────
9
+ /**
10
+ * Check if FTS5 tables exist (migration v2 applied).
11
+ */
12
+ function hasFts(db) {
13
+ try {
14
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='fts_sessions'").get();
15
+ return !!row;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Escape a user query for FTS5 MATCH syntax.
23
+ * Wraps each word in quotes to avoid syntax errors from special chars.
24
+ */
25
+ function ftsEscape(query) {
26
+ return query
27
+ .split(/\s+/)
28
+ .filter(Boolean)
29
+ .map(word => `"${word.replace(/"/g, '""')}"`)
30
+ .join(" ");
31
+ }
32
+ export function registerIntelligenceTools(server) {
33
+ // ─── SCAN PROJECT ───────────────────────────────────────────────────
34
+ server.registerTool(`${TOOL_PREFIX}_scan_project`, {
35
+ title: "Scan Project",
36
+ description: `Scan the project filesystem and build a cached snapshot of the structure. Includes file tree, auto-detected architectural layers, existing file notes, active decisions, and conventions. The snapshot is cached and reused — no need to rescan unless files changed significantly.
37
+
38
+ Args:
39
+ - force_refresh (boolean, optional): Force rescan even if cache is fresh (default: false)
40
+ - max_depth (number, optional): Max directory depth to scan (default: 5)
41
+
42
+ Returns:
43
+ ProjectSnapshot with file tree, layer distribution, and all stored intelligence.`,
44
+ inputSchema: {
45
+ force_refresh: z.boolean().default(false).describe("Force rescan even if cache is fresh"),
46
+ max_depth: z.number().int().min(1).max(10).default(5).describe("Max directory depth"),
47
+ },
48
+ annotations: {
49
+ readOnlyHint: false, // It writes to cache
50
+ destructiveHint: false,
51
+ idempotentHint: true,
52
+ openWorldHint: false,
53
+ },
54
+ }, async ({ force_refresh, max_depth }) => {
55
+ const db = getDb();
56
+ const projectRoot = getProjectRoot();
57
+ // Check cache freshness
58
+ if (!force_refresh) {
59
+ const cached = db.prepare("SELECT * FROM snapshot_cache WHERE key = 'project_structure'").get();
60
+ if (cached) {
61
+ const age = minutesSince(cached.updated_at);
62
+ if (age < SNAPSHOT_TTL_MINUTES) {
63
+ const snapshot = safeJsonParse(cached.value, null);
64
+ if (snapshot) {
65
+ return {
66
+ content: [{
67
+ type: "text",
68
+ text: JSON.stringify({
69
+ ...snapshot,
70
+ _cache_status: `fresh (${age}min old, TTL: ${SNAPSHOT_TTL_MINUTES}min)`,
71
+ }, null, 2),
72
+ }],
73
+ };
74
+ }
75
+ }
76
+ }
77
+ }
78
+ // Perform full scan
79
+ const fileTree = scanFileTree(projectRoot, max_depth);
80
+ // Auto-detect layers for each file
81
+ const layerDist = {};
82
+ for (const f of fileTree) {
83
+ if (f.endsWith("/"))
84
+ continue; // Skip directories
85
+ const layer = detectLayer(f);
86
+ layerDist[layer] = (layerDist[layer] || 0) + 1;
87
+ }
88
+ // Fetch stored intelligence
89
+ const fileNotes = db.prepare("SELECT * FROM file_notes ORDER BY file_path").all();
90
+ const activeDecisions = db.prepare("SELECT * FROM decisions WHERE status = 'active' ORDER BY timestamp DESC LIMIT 20").all();
91
+ const activeConventions = db.prepare("SELECT * FROM conventions WHERE enforced = 1 ORDER BY category").all();
92
+ // Git info
93
+ let gitInfo = null;
94
+ if (isGitRepo(projectRoot)) {
95
+ const branch = getGitBranch(projectRoot);
96
+ const head = getGitHead(projectRoot);
97
+ gitInfo = { branch, head, is_clean: true }; // Simplified
98
+ }
99
+ const snapshot = {
100
+ project_root: projectRoot,
101
+ file_tree: fileTree,
102
+ total_files: fileTree.filter(f => !f.endsWith("/")).length,
103
+ file_notes: fileNotes,
104
+ recent_decisions: activeDecisions,
105
+ active_conventions: activeConventions,
106
+ layer_distribution: layerDist,
107
+ generated_at: now(),
108
+ git: gitInfo,
109
+ };
110
+ // Cache the snapshot
111
+ db.prepare("INSERT OR REPLACE INTO snapshot_cache (key, value, updated_at, ttl_minutes) VALUES ('project_structure', ?, ?, ?)").run(JSON.stringify(snapshot), now(), SNAPSHOT_TTL_MINUTES);
112
+ return {
113
+ content: [{
114
+ type: "text",
115
+ text: JSON.stringify({
116
+ ...snapshot,
117
+ _cache_status: "freshly scanned",
118
+ }, null, 2),
119
+ }],
120
+ };
121
+ });
122
+ // ─── SEARCH MEMORY (FTS5-powered) ──────────────────────────────────
123
+ server.registerTool(`${TOOL_PREFIX}_search`, {
124
+ title: "Search Memory",
125
+ description: `Full-text search across ALL memory: sessions, changes, decisions, file notes, conventions, and tasks. Uses FTS5 for high-performance ranked results when available, falls back to LIKE for compatibility.
126
+
127
+ Args:
128
+ - query (string): Search term(s)
129
+ - scope (string, optional): Limit search to a specific table — "sessions", "changes", "decisions", "file_notes", "conventions", "tasks", or "all" (default: "all")
130
+ - limit (number, optional): Max total results (default: 20)
131
+
132
+ Returns:
133
+ Grouped search results with relevance context.`,
134
+ inputSchema: {
135
+ query: z.string().min(1).describe("Search term(s)"),
136
+ scope: z.enum(["all", "sessions", "changes", "decisions", "file_notes", "conventions", "tasks"]).default("all"),
137
+ limit: z.number().int().min(1).max(MAX_SEARCH_RESULTS).default(20),
138
+ },
139
+ annotations: {
140
+ readOnlyHint: true,
141
+ destructiveHint: false,
142
+ idempotentHint: true,
143
+ openWorldHint: false,
144
+ },
145
+ }, async ({ query, scope, limit }) => {
146
+ const db = getDb();
147
+ const useFts = hasFts(db);
148
+ const results = {};
149
+ let totalFound = 0;
150
+ const perTable = Math.ceil(limit / 6);
151
+ if (useFts) {
152
+ // ─── FTS5 Path (fast, ranked) ────────────────────────────
153
+ const ftsQuery = ftsEscape(query);
154
+ if (scope === "all" || scope === "sessions") {
155
+ try {
156
+ const rows = db.prepare(`SELECT s.*, rank FROM fts_sessions f
157
+ JOIN sessions s ON s.id = f.rowid
158
+ WHERE fts_sessions MATCH ?
159
+ ORDER BY rank LIMIT ?`).all(ftsQuery, perTable);
160
+ if (rows.length) {
161
+ results.sessions = rows;
162
+ totalFound += rows.length;
163
+ }
164
+ }
165
+ catch { /* FTS match failed, skip */ }
166
+ }
167
+ if (scope === "all" || scope === "changes") {
168
+ try {
169
+ const rows = db.prepare(`SELECT c.*, rank FROM fts_changes f
170
+ JOIN changes c ON c.id = f.rowid
171
+ WHERE fts_changes MATCH ?
172
+ ORDER BY rank LIMIT ?`).all(ftsQuery, perTable);
173
+ if (rows.length) {
174
+ results.changes = rows;
175
+ totalFound += rows.length;
176
+ }
177
+ }
178
+ catch { /* FTS match failed, skip */ }
179
+ }
180
+ if (scope === "all" || scope === "decisions") {
181
+ try {
182
+ const rows = db.prepare(`SELECT d.*, rank FROM fts_decisions f
183
+ JOIN decisions d ON d.id = f.rowid
184
+ WHERE fts_decisions MATCH ?
185
+ ORDER BY rank LIMIT ?`).all(ftsQuery, perTable);
186
+ if (rows.length) {
187
+ results.decisions = rows;
188
+ totalFound += rows.length;
189
+ }
190
+ }
191
+ catch { /* FTS match failed, skip */ }
192
+ }
193
+ if (scope === "all" || scope === "file_notes") {
194
+ try {
195
+ const rows = db.prepare(`SELECT * FROM file_notes WHERE file_path IN (
196
+ SELECT file_path FROM fts_file_notes WHERE fts_file_notes MATCH ?
197
+ ) LIMIT ?`).all(ftsQuery, perTable);
198
+ if (rows.length) {
199
+ results.file_notes = rows;
200
+ totalFound += rows.length;
201
+ }
202
+ }
203
+ catch { /* FTS match failed, skip */ }
204
+ }
205
+ if (scope === "all" || scope === "conventions") {
206
+ try {
207
+ const rows = db.prepare(`SELECT c.*, rank FROM fts_conventions f
208
+ JOIN conventions c ON c.id = f.rowid
209
+ WHERE fts_conventions MATCH ?
210
+ ORDER BY rank LIMIT ?`).all(ftsQuery, perTable);
211
+ if (rows.length) {
212
+ results.conventions = rows;
213
+ totalFound += rows.length;
214
+ }
215
+ }
216
+ catch { /* FTS match failed, skip */ }
217
+ }
218
+ if (scope === "all" || scope === "tasks") {
219
+ try {
220
+ const rows = db.prepare(`SELECT t.*, rank FROM fts_tasks f
221
+ JOIN tasks t ON t.id = f.rowid
222
+ WHERE fts_tasks MATCH ?
223
+ ORDER BY rank LIMIT ?`).all(ftsQuery, perTable);
224
+ if (rows.length) {
225
+ results.tasks = rows;
226
+ totalFound += rows.length;
227
+ }
228
+ }
229
+ catch { /* FTS match failed, skip */ }
230
+ }
231
+ }
232
+ else {
233
+ // ─── LIKE Fallback (slow but compatible) ─────────────────
234
+ const term = `%${query}%`;
235
+ if (scope === "all" || scope === "sessions") {
236
+ const rows = db.prepare("SELECT * FROM sessions WHERE summary LIKE ? OR tags LIKE ? ORDER BY id DESC LIMIT ?").all(term, term, perTable);
237
+ if (rows.length) {
238
+ results.sessions = rows;
239
+ totalFound += rows.length;
240
+ }
241
+ }
242
+ if (scope === "all" || scope === "changes") {
243
+ const rows = db.prepare("SELECT * FROM changes WHERE description LIKE ? OR file_path LIKE ? OR diff_summary LIKE ? ORDER BY timestamp DESC LIMIT ?").all(term, term, term, perTable);
244
+ if (rows.length) {
245
+ results.changes = rows;
246
+ totalFound += rows.length;
247
+ }
248
+ }
249
+ if (scope === "all" || scope === "decisions") {
250
+ const rows = db.prepare("SELECT * FROM decisions WHERE decision LIKE ? OR rationale LIKE ? OR tags LIKE ? ORDER BY timestamp DESC LIMIT ?").all(term, term, term, perTable);
251
+ if (rows.length) {
252
+ results.decisions = rows;
253
+ totalFound += rows.length;
254
+ }
255
+ }
256
+ if (scope === "all" || scope === "file_notes") {
257
+ const rows = db.prepare("SELECT * FROM file_notes WHERE file_path LIKE ? OR purpose LIKE ? OR notes LIKE ? LIMIT ?").all(term, term, term, perTable);
258
+ if (rows.length) {
259
+ results.file_notes = rows;
260
+ totalFound += rows.length;
261
+ }
262
+ }
263
+ if (scope === "all" || scope === "conventions") {
264
+ const rows = db.prepare("SELECT * FROM conventions WHERE rule LIKE ? OR examples LIKE ? LIMIT ?").all(term, term, perTable);
265
+ if (rows.length) {
266
+ results.conventions = rows;
267
+ totalFound += rows.length;
268
+ }
269
+ }
270
+ if (scope === "all" || scope === "tasks") {
271
+ const rows = db.prepare("SELECT * FROM tasks WHERE title LIKE ? OR description LIKE ? OR tags LIKE ? ORDER BY updated_at DESC LIMIT ?").all(term, term, term, perTable);
272
+ if (rows.length) {
273
+ results.tasks = rows;
274
+ totalFound += rows.length;
275
+ }
276
+ }
277
+ }
278
+ return {
279
+ content: [{
280
+ type: "text",
281
+ text: JSON.stringify({
282
+ query,
283
+ scope,
284
+ search_engine: useFts ? "fts5" : "like",
285
+ total_results: totalFound,
286
+ results,
287
+ }, null, 2),
288
+ }],
289
+ };
290
+ });
291
+ // ─── WHAT CHANGED ───────────────────────────────────────────────────
292
+ server.registerTool(`${TOOL_PREFIX}_what_changed`, {
293
+ title: "What Changed",
294
+ description: `Comprehensive diff report: what changed since a given time. Combines agent-recorded changes with git history. Use to quickly catch up after being away.
295
+
296
+ Args:
297
+ - since (string, optional): ISO timestamp or relative like "1h", "24h", "7d" (default: last session end)
298
+ - include_git (boolean, optional): Include git log (default: true)
299
+
300
+ Returns:
301
+ Combined change report from both agent memory and git.`,
302
+ inputSchema: {
303
+ since: z.string().optional().describe('ISO timestamp or relative: "1h", "24h", "7d"'),
304
+ include_git: z.boolean().default(true),
305
+ },
306
+ annotations: {
307
+ readOnlyHint: true,
308
+ destructiveHint: false,
309
+ idempotentHint: true,
310
+ openWorldHint: false,
311
+ },
312
+ }, async ({ since, include_git }) => {
313
+ const db = getDb();
314
+ const projectRoot = getProjectRoot();
315
+ // Resolve "since" to an ISO timestamp
316
+ let sinceTimestamp;
317
+ if (!since) {
318
+ // Default to last session end
319
+ const last = db.prepare("SELECT ended_at FROM sessions WHERE ended_at IS NOT NULL ORDER BY id DESC LIMIT 1").get();
320
+ sinceTimestamp = last?.ended_at || new Date(Date.now() - 86400000).toISOString();
321
+ }
322
+ else if (/^\d+[hdm]$/.test(since)) {
323
+ // Relative time
324
+ const match = since.match(/^(\d+)([hdm])$/);
325
+ if (match) {
326
+ const amount = parseInt(match[1]);
327
+ const unit = match[2];
328
+ const ms = unit === "h" ? amount * 3600000 : unit === "d" ? amount * 86400000 : amount * 60000;
329
+ sinceTimestamp = new Date(Date.now() - ms).toISOString();
330
+ }
331
+ else {
332
+ sinceTimestamp = since;
333
+ }
334
+ }
335
+ else {
336
+ sinceTimestamp = since;
337
+ }
338
+ // Agent-recorded changes
339
+ const agentChanges = db.prepare("SELECT * FROM changes WHERE timestamp > ? ORDER BY timestamp DESC").all(sinceTimestamp);
340
+ // Decisions made since
341
+ const newDecisions = db.prepare("SELECT * FROM decisions WHERE timestamp > ? ORDER BY timestamp DESC").all(sinceTimestamp);
342
+ // Git changes
343
+ let gitLog = "";
344
+ let gitFilesChanged = [];
345
+ if (include_git && isGitRepo(projectRoot)) {
346
+ gitLog = getGitLogSince(projectRoot, sinceTimestamp);
347
+ gitFilesChanged = getGitFilesChanged(projectRoot, sinceTimestamp);
348
+ }
349
+ // Files only in git (not recorded by agent)
350
+ const recordedFiles = new Set(agentChanges.map(c => c.file_path));
351
+ const unrecordedGitChanges = gitFilesChanged.filter(f => !recordedFiles.has(f));
352
+ return {
353
+ content: [{
354
+ type: "text",
355
+ text: JSON.stringify({
356
+ since: sinceTimestamp,
357
+ agent_recorded: {
358
+ count: agentChanges.length,
359
+ changes: agentChanges,
360
+ },
361
+ new_decisions: newDecisions,
362
+ git: include_git ? {
363
+ log: gitLog,
364
+ files_changed: gitFilesChanged.length,
365
+ unrecorded_changes: unrecordedGitChanges,
366
+ } : null,
367
+ summary: `${agentChanges.length} recorded changes, ${newDecisions.length} new decisions, ${gitFilesChanged.length} git file changes (${unrecordedGitChanges.length} unrecorded) since ${sinceTimestamp}.`,
368
+ }, null, 2),
369
+ }],
370
+ };
371
+ });
372
+ // ─── GET DEPENDENCY MAP ─────────────────────────────────────────────
373
+ server.registerTool(`${TOOL_PREFIX}_get_dependency_map`, {
374
+ title: "Get Dependency Map",
375
+ description: `Get the dependency graph for a file: what it depends on and what depends on it. Built from stored file notes.
376
+
377
+ Args:
378
+ - file_path (string): File to query
379
+ - depth (number, optional): How many levels deep to traverse (default: 1)
380
+
381
+ Returns:
382
+ Dependency tree with upstream and downstream files.`,
383
+ inputSchema: {
384
+ file_path: z.string().describe("File to query"),
385
+ depth: z.number().int().min(1).max(5).default(1).describe("Traversal depth"),
386
+ },
387
+ annotations: {
388
+ readOnlyHint: true,
389
+ destructiveHint: false,
390
+ idempotentHint: true,
391
+ openWorldHint: false,
392
+ },
393
+ }, async ({ file_path, depth }) => {
394
+ const db = getDb();
395
+ function getDeps(fp, dir, currentDepth) {
396
+ if (currentDepth > depth)
397
+ return {};
398
+ const note = db.prepare("SELECT * FROM file_notes WHERE file_path = ?").get(fp);
399
+ if (!note)
400
+ return {};
401
+ const field = dir === "up" ? "dependencies" : "dependents";
402
+ const deps = safeJsonParse(note[field], []);
403
+ const result = {};
404
+ for (const dep of deps) {
405
+ result[dep] = getDeps(dep, dir, currentDepth + 1);
406
+ }
407
+ return result;
408
+ }
409
+ const upstream = getDeps(file_path, "up", 1);
410
+ const downstream = getDeps(file_path, "down", 1);
411
+ const note = db.prepare("SELECT * FROM file_notes WHERE file_path = ?").get(file_path);
412
+ return {
413
+ content: [{
414
+ type: "text",
415
+ text: JSON.stringify({
416
+ file_path,
417
+ purpose: note?.purpose || "(no notes recorded)",
418
+ layer: note?.layer || detectLayer(file_path),
419
+ complexity: note?.complexity || "unknown",
420
+ depends_on: upstream,
421
+ depended_by: downstream,
422
+ }, null, 2),
423
+ }],
424
+ };
425
+ });
426
+ }
427
+ //# sourceMappingURL=intelligence.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerMaintenanceTools(server: McpServer): void;
3
+ //# sourceMappingURL=maintenance.d.ts.map