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.
- package/README.md +645 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +81 -0
- package/dist/database.d.ts +30 -0
- package/dist/database.js +134 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +67 -0
- package/dist/migrations.d.ts +4 -0
- package/dist/migrations.js +342 -0
- package/dist/scripts/install-hooks.d.ts +3 -0
- package/dist/scripts/install-hooks.js +89 -0
- package/dist/tools/intelligence.d.ts +3 -0
- package/dist/tools/intelligence.js +427 -0
- package/dist/tools/maintenance.d.ts +3 -0
- package/dist/tools/maintenance.js +646 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.js +446 -0
- package/dist/tools/scheduler.d.ts +3 -0
- package/dist/tools/scheduler.js +363 -0
- package/dist/tools/sessions.d.ts +3 -0
- package/dist/tools/sessions.js +355 -0
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.js +206 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +58 -0
- package/dist/utils.js +190 -0
- package/docs/scheduled-events.md +150 -0
- package/package.json +43 -0
- package/scripts/install-mcp.js +175 -0
- package/src/constants.ts +86 -0
- package/src/database.ts +162 -0
- package/src/index.ts +79 -0
- package/src/migrations.ts +367 -0
- package/src/scripts/install-hooks.ts +96 -0
- package/src/tools/intelligence.ts +469 -0
- package/src/tools/maintenance.ts +783 -0
- package/src/tools/memory.ts +543 -0
- package/src/tools/scheduler.ts +413 -0
- package/src/tools/sessions.ts +430 -0
- package/src/tools/tasks.ts +215 -0
- package/src/types.ts +267 -0
- package/src/utils.ts +216 -0
- 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,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
|