repomemory 1.0.4 → 1.1.1
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/.claude-plugin/plugin.json +2 -2
- package/README.md +93 -34
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +20 -0
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +305 -44
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/go.d.ts +6 -0
- package/dist/commands/go.d.ts.map +1 -0
- package/dist/commands/go.js +132 -0
- package/dist/commands/go.js.map +1 -0
- package/dist/commands/hook.d.ts.map +1 -1
- package/dist/commands/hook.js +19 -4
- package/dist/commands/hook.js.map +1 -1
- package/dist/commands/init.d.ts +3 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +51 -38
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +5 -2
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/setup.js +7 -1
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +3 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/wizard.d.ts.map +1 -1
- package/dist/commands/wizard.js +11 -3
- package/dist/commands/wizard.js.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +9 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +8 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-store.d.ts.map +1 -1
- package/dist/lib/context-store.js +18 -3
- package/dist/lib/context-store.js.map +1 -1
- package/dist/lib/embeddings.d.ts +27 -0
- package/dist/lib/embeddings.d.ts.map +1 -0
- package/dist/lib/embeddings.js +121 -0
- package/dist/lib/embeddings.js.map +1 -0
- package/dist/lib/search.d.ts +9 -2
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +259 -21
- package/dist/lib/search.js.map +1 -1
- package/dist/mcp/server.d.ts +26 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +333 -43
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
- package/server.json +12 -8
- package/skills/repomemory/SKILL.md +7 -4
- package/skills/session-end/SKILL.md +19 -0
- package/skills/session-start/SKILL.md +14 -0
package/dist/mcp/server.js
CHANGED
|
@@ -3,22 +3,100 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { ContextStore } from "../lib/context-store.js";
|
|
5
5
|
import { SearchIndex } from "../lib/search.js";
|
|
6
|
-
|
|
6
|
+
import { createEmbeddingProvider } from "../lib/embeddings.js";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { version: PKG_VERSION } = require("../../package.json");
|
|
10
|
+
const VALID_CATEGORIES = ["facts", "decisions", "regressions", "sessions", "changelog", "preferences"];
|
|
11
|
+
function createSessionTracker() {
|
|
12
|
+
return {
|
|
13
|
+
startTime: new Date(),
|
|
14
|
+
toolCalls: [],
|
|
15
|
+
searchQueries: [],
|
|
16
|
+
entriesRead: [],
|
|
17
|
+
entriesWritten: [],
|
|
18
|
+
entriesDeleted: [],
|
|
19
|
+
writeCallMade: false,
|
|
20
|
+
readCallCount: 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function buildSessionSummary(session, durationSeconds) {
|
|
24
|
+
const date = new Date().toISOString().split("T")[0];
|
|
25
|
+
const mins = Math.round(durationSeconds / 60);
|
|
26
|
+
const parts = [];
|
|
27
|
+
parts.push(`## Auto-captured session ${date} (${mins}min)\n`);
|
|
28
|
+
if (session.searchQueries.length > 0) {
|
|
29
|
+
parts.push(`**Searched:** ${[...new Set(session.searchQueries)].join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
if (session.entriesRead.length > 0) {
|
|
32
|
+
parts.push(`**Read:** ${[...new Set(session.entriesRead)].join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
if (session.entriesWritten.length > 0) {
|
|
35
|
+
parts.push(`**Written:** ${[...new Set(session.entriesWritten)].join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
if (session.entriesDeleted.length > 0) {
|
|
38
|
+
parts.push(`**Deleted:** ${[...new Set(session.entriesDeleted)].join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
parts.push(`**Total tool calls:** ${session.toolCalls.length}`);
|
|
41
|
+
return parts.join("\n");
|
|
42
|
+
}
|
|
43
|
+
// --- Intelligent Category Routing ---
|
|
44
|
+
/**
|
|
45
|
+
* Detect the most likely category for a search query using keyword heuristics.
|
|
46
|
+
* Returns undefined if no category can be confidently inferred.
|
|
47
|
+
*
|
|
48
|
+
* Precedence order is intentional: decisions > regressions > preferences > sessions > facts.
|
|
49
|
+
* For ambiguous queries (e.g., "why did the login crash"), decisions wins because
|
|
50
|
+
* understanding the "why" is usually more actionable. The caller retries without
|
|
51
|
+
* category filter if the routed search returns 0 results.
|
|
52
|
+
*/
|
|
53
|
+
export function detectQueryCategory(query) {
|
|
54
|
+
const q = query.toLowerCase();
|
|
55
|
+
// Decision-related queries — "why" is the strongest signal
|
|
56
|
+
if (/\b(why\b|chose|decision|alternatives?|trade.?off|instead of|reason\b)/.test(q))
|
|
57
|
+
return "decisions";
|
|
58
|
+
// Regression/bug queries
|
|
59
|
+
if (/\b(bug|broke|regression|crash|error\b|fail|fix\b|issues?\b|problem|broken)/.test(q))
|
|
60
|
+
return "regressions";
|
|
61
|
+
// Preference/style queries — require coding/style context to avoid false positives
|
|
62
|
+
if (/\b(prefer(?:red|ence|s)?|coding style|naming convention|indent(?:ation)?|lint(?:ing)?|tab(?:s|\s+vs|\s+or)|code format(?:ting)?)/.test(q))
|
|
63
|
+
return "preferences";
|
|
64
|
+
// Session queries
|
|
65
|
+
if (/\b(last session|previous session|yesterday|worked on|accomplished)/.test(q))
|
|
66
|
+
return "sessions";
|
|
67
|
+
// Architecture/fact queries
|
|
68
|
+
if (/\b(how does|architecture|schema|database|api|endpoint|flow|structure)/.test(q))
|
|
69
|
+
return "facts";
|
|
70
|
+
return undefined; // search all categories
|
|
71
|
+
}
|
|
7
72
|
export async function startMcpServer(repoRoot, config) {
|
|
8
73
|
const store = new ContextStore(repoRoot, config);
|
|
9
74
|
let searchIndex = null;
|
|
75
|
+
// Initialize embedding provider (optional — falls back to keyword search)
|
|
76
|
+
let embeddingProvider = null;
|
|
77
|
+
try {
|
|
78
|
+
embeddingProvider = await createEmbeddingProvider({
|
|
79
|
+
provider: config.embeddingProvider,
|
|
80
|
+
model: config.embeddingModel,
|
|
81
|
+
apiKey: config.embeddingApiKey,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// No embeddings available, will use keyword-only search
|
|
86
|
+
}
|
|
10
87
|
if (store.exists()) {
|
|
11
88
|
try {
|
|
12
|
-
searchIndex = new SearchIndex(store.path, store);
|
|
89
|
+
searchIndex = new SearchIndex(store.path, store, embeddingProvider, config.hybridAlpha);
|
|
13
90
|
await searchIndex.rebuild();
|
|
14
91
|
}
|
|
15
92
|
catch (e) {
|
|
16
93
|
console.error("Warning: Could not initialize search index:", e);
|
|
17
94
|
}
|
|
18
95
|
}
|
|
96
|
+
const session = createSessionTracker();
|
|
19
97
|
const server = new Server({
|
|
20
98
|
name: "repomemory",
|
|
21
|
-
version:
|
|
99
|
+
version: PKG_VERSION,
|
|
22
100
|
}, {
|
|
23
101
|
capabilities: {
|
|
24
102
|
tools: {},
|
|
@@ -42,7 +120,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
42
120
|
},
|
|
43
121
|
{
|
|
44
122
|
name: "end-session",
|
|
45
|
-
description: "Record what you accomplished and discovered during this session.",
|
|
123
|
+
description: "Record what you accomplished and discovered during this session. Routes conclusions to the right categories.",
|
|
46
124
|
arguments: [
|
|
47
125
|
{
|
|
48
126
|
name: "summary",
|
|
@@ -64,7 +142,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
64
142
|
role: "user",
|
|
65
143
|
content: {
|
|
66
144
|
type: "text",
|
|
67
|
-
text: `I'm about to work on: ${task}\n\
|
|
145
|
+
text: `I'm about to work on: ${task}\n\nYou MUST search the repository's persistent knowledge base for relevant context before starting. Use context_search with relevant keywords, and call context_auto_orient if this is a new session. Do NOT skip this step.`,
|
|
68
146
|
},
|
|
69
147
|
},
|
|
70
148
|
],
|
|
@@ -79,7 +157,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
79
157
|
role: "user",
|
|
80
158
|
content: {
|
|
81
159
|
type: "text",
|
|
82
|
-
text: `Session summary: ${summary}\n\nPlease record this
|
|
160
|
+
text: `Session summary: ${summary}\n\nPlease record this session's work. IMPORTANT: Route knowledge to the RIGHT categories:\n- New architectural facts \u2192 context_write(category="facts", ...)\n- Decisions made \u2192 context_write(category="decisions", ...)\n- Bugs/regressions found \u2192 context_write(category="regressions", ...)\n- Coding style preferences \u2192 context_write(category="preferences", ...)\n- The session overview itself \u2192 context_write(category="sessions", ...)\n\nDo NOT dump everything into sessions/. Parse your conclusions and write each piece to the appropriate category.`,
|
|
83
161
|
},
|
|
84
162
|
},
|
|
85
163
|
],
|
|
@@ -110,6 +188,11 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
110
188
|
type: "number",
|
|
111
189
|
description: "Max results to return (default: 5)",
|
|
112
190
|
},
|
|
191
|
+
detail: {
|
|
192
|
+
type: "string",
|
|
193
|
+
enum: ["compact", "full"],
|
|
194
|
+
description: "Level of detail. 'compact' (default) returns one-line summaries (~50 tokens each). 'full' returns longer snippets.",
|
|
195
|
+
},
|
|
113
196
|
},
|
|
114
197
|
required: ["query"],
|
|
115
198
|
},
|
|
@@ -123,19 +206,14 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
123
206
|
},
|
|
124
207
|
{
|
|
125
208
|
name: "context_write",
|
|
126
|
-
description: "Write a new piece of knowledge to the repository's persistent memory. Use this to record: discoveries you made during this session, architectural decisions, bug patterns, or any insight that would help a future AI session. This persists across sessions
|
|
209
|
+
description: "Write a new piece of knowledge to the repository's persistent memory. Use this to record: discoveries you made during this session, architectural decisions, bug patterns, or any insight that would help a future AI session. This persists across sessions \u2014 write anything you'd want a future version of yourself to know.",
|
|
127
210
|
inputSchema: {
|
|
128
211
|
type: "object",
|
|
129
212
|
properties: {
|
|
130
213
|
category: {
|
|
131
214
|
type: "string",
|
|
132
215
|
enum: VALID_CATEGORIES,
|
|
133
|
-
description: `Category for the knowledge:
|
|
134
|
-
- facts: Architecture, patterns, how things work
|
|
135
|
-
- decisions: Why something was chosen (include alternatives considered)
|
|
136
|
-
- regressions: Bug patterns, things that broke, gotchas
|
|
137
|
-
- sessions: What you worked on and discovered this session
|
|
138
|
-
- changelog: Notable changes`,
|
|
216
|
+
description: `Category for the knowledge:\n- facts: Architecture, patterns, how things work\n- decisions: Why something was chosen (include alternatives considered)\n- regressions: Bug patterns, things that broke, gotchas\n- sessions: What you worked on and discovered this session\n- changelog: Notable changes\n- preferences: Coding style, preferred patterns, tool configs, formatting rules \u2014 personal developer knowledge`,
|
|
139
217
|
},
|
|
140
218
|
filename: {
|
|
141
219
|
type: "string",
|
|
@@ -149,6 +227,10 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
149
227
|
type: "boolean",
|
|
150
228
|
description: "If true, append to existing file instead of overwriting. Useful for session logs.",
|
|
151
229
|
},
|
|
230
|
+
supersedes: {
|
|
231
|
+
type: "string",
|
|
232
|
+
description: "Filename of an existing entry in the same category that this replaces. The old entry will be auto-deleted.",
|
|
233
|
+
},
|
|
152
234
|
},
|
|
153
235
|
required: ["category", "filename", "content"],
|
|
154
236
|
},
|
|
@@ -162,7 +244,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
162
244
|
},
|
|
163
245
|
{
|
|
164
246
|
name: "context_delete",
|
|
165
|
-
description: "Delete a knowledge entry from the repository context. Use this to remove stale or incorrect information. Knowledge quality matters more than quantity
|
|
247
|
+
description: "Delete a knowledge entry from the repository context. Use this to remove stale or incorrect information. Knowledge quality matters more than quantity \u2014 prune aggressively.",
|
|
166
248
|
inputSchema: {
|
|
167
249
|
type: "object",
|
|
168
250
|
properties: {
|
|
@@ -197,6 +279,10 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
197
279
|
enum: VALID_CATEGORIES,
|
|
198
280
|
description: "Optional: filter to a specific category.",
|
|
199
281
|
},
|
|
282
|
+
compact: {
|
|
283
|
+
type: "boolean",
|
|
284
|
+
description: "If true (default), returns one-line summaries. If false, includes file sizes.",
|
|
285
|
+
},
|
|
200
286
|
},
|
|
201
287
|
},
|
|
202
288
|
annotations: {
|
|
@@ -215,7 +301,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
215
301
|
properties: {
|
|
216
302
|
category: {
|
|
217
303
|
type: "string",
|
|
218
|
-
description: "The category (facts, decisions, regressions, sessions, changelog)",
|
|
304
|
+
description: "The category (facts, decisions, regressions, sessions, changelog, preferences)",
|
|
219
305
|
},
|
|
220
306
|
filename: {
|
|
221
307
|
type: "string",
|
|
@@ -232,14 +318,40 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
232
318
|
openWorldHint: false,
|
|
233
319
|
},
|
|
234
320
|
},
|
|
321
|
+
{
|
|
322
|
+
name: "context_auto_orient",
|
|
323
|
+
description: "Get a comprehensive project orientation in a single call. Returns the project index, recent session summaries, and recently modified entries. Use this at the START of every new coding session to immediately understand the project. This replaces the need to make 3-4 separate tool calls.",
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: "object",
|
|
326
|
+
properties: {},
|
|
327
|
+
},
|
|
328
|
+
annotations: {
|
|
329
|
+
title: "Auto Orient",
|
|
330
|
+
readOnlyHint: true,
|
|
331
|
+
destructiveHint: false,
|
|
332
|
+
idempotentHint: true,
|
|
333
|
+
openWorldHint: false,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
235
336
|
],
|
|
236
337
|
};
|
|
237
338
|
});
|
|
238
339
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
239
340
|
const { name, arguments: args } = request.params;
|
|
341
|
+
// Track every tool call for session capture
|
|
342
|
+
session.toolCalls.push({ tool: name, timestamp: new Date() });
|
|
343
|
+
// Build write-nudge suffix for non-writing sessions
|
|
344
|
+
const getWriteNudge = () => {
|
|
345
|
+
if (!session.writeCallMade && session.readCallCount >= 3) {
|
|
346
|
+
return "\n\n> Tip: Use `context_write` to record any discoveries or decisions from this session.";
|
|
347
|
+
}
|
|
348
|
+
return "";
|
|
349
|
+
};
|
|
240
350
|
switch (name) {
|
|
241
351
|
case "context_search": {
|
|
242
|
-
const { query, category, limit = 5 } = args;
|
|
352
|
+
const { query, category, limit = 5, detail = "compact" } = args;
|
|
353
|
+
session.searchQueries.push(query);
|
|
354
|
+
session.readCallCount++;
|
|
243
355
|
// Validate category if provided
|
|
244
356
|
if (category && !VALID_CATEGORIES.includes(category)) {
|
|
245
357
|
return {
|
|
@@ -254,7 +366,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
254
366
|
return {
|
|
255
367
|
content: [{
|
|
256
368
|
type: "text",
|
|
257
|
-
text: "No .context/ directory found.
|
|
369
|
+
text: "No .context/ directory found. Tell the user to run:\n\n npx repomemory go\n\nThis will set up persistent memory for this project.",
|
|
258
370
|
}],
|
|
259
371
|
};
|
|
260
372
|
}
|
|
@@ -262,9 +374,24 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
262
374
|
searchIndex = new SearchIndex(store.path, store);
|
|
263
375
|
await searchIndex.rebuild();
|
|
264
376
|
}
|
|
265
|
-
|
|
377
|
+
// Intelligent category routing: auto-detect if not explicitly provided
|
|
378
|
+
let effectiveCategory = category;
|
|
379
|
+
let routingNote = "";
|
|
380
|
+
if (!category) {
|
|
381
|
+
const detected = detectQueryCategory(query);
|
|
382
|
+
if (detected) {
|
|
383
|
+
effectiveCategory = detected;
|
|
384
|
+
routingNote = `(auto-routed to ${detected}/) `;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
let results = await searchIndex.search(query, effectiveCategory, limit);
|
|
388
|
+
// If routing returned 0 results, retry without category filter
|
|
389
|
+
if (results.length === 0 && effectiveCategory && !category) {
|
|
390
|
+
results = await searchIndex.search(query, undefined, limit);
|
|
391
|
+
routingNote = "";
|
|
392
|
+
}
|
|
266
393
|
if (results.length === 0) {
|
|
267
|
-
// Fallback to simple text search
|
|
394
|
+
// Fallback to simple text search (use explicit category, not auto-routed)
|
|
268
395
|
const entries = store.listEntries(category);
|
|
269
396
|
const queryLower = query.toLowerCase();
|
|
270
397
|
const matched = entries
|
|
@@ -275,22 +402,42 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
275
402
|
return {
|
|
276
403
|
content: [{
|
|
277
404
|
type: "text",
|
|
278
|
-
text: `No results found for "${query}"${category ? ` in ${category}` : ""}. Try a different query or browse with context_list
|
|
405
|
+
text: `No results found for "${query}"${category ? ` in ${category}` : ""}. Try a different query or browse with context_list.${getWriteNudge()}`,
|
|
279
406
|
}],
|
|
280
407
|
};
|
|
281
408
|
}
|
|
282
|
-
|
|
283
|
-
|
|
409
|
+
// Format fallback results respecting detail level
|
|
410
|
+
let text;
|
|
411
|
+
if (detail === "compact") {
|
|
412
|
+
text = routingNote + matched
|
|
413
|
+
.map((e) => `- **${e.title}** [${e.category}/${e.filename}] \u2014 ${e.content.slice(0, 150).replace(/\n/g, " ")}...`)
|
|
414
|
+
.join("\n");
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
text = routingNote + matched
|
|
418
|
+
.map((e) => `## ${e.category}/${e.filename}\n**${e.title}**\n\n${e.content.slice(0, 800)}\n`)
|
|
419
|
+
.join("\n---\n\n");
|
|
420
|
+
}
|
|
421
|
+
return { content: [{ type: "text", text: text + getWriteNudge() }] };
|
|
422
|
+
}
|
|
423
|
+
// Format search results based on detail level
|
|
424
|
+
let text;
|
|
425
|
+
if (detail === "compact") {
|
|
426
|
+
text = routingNote + results
|
|
427
|
+
.map((r) => `- **${r.title}** [${r.category}/${r.filename}] (score: ${r.score.toFixed(2)}) \u2014 ${r.snippet.slice(0, 150).replace(/\n/g, " ")}...`)
|
|
428
|
+
.join("\n");
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
text = routingNote + results
|
|
432
|
+
.map((r) => `## ${r.category}/${r.filename} (relevance: ${r.score.toFixed(2)})\n**${r.title}**\n\n${r.snippet}\n`)
|
|
284
433
|
.join("\n---\n\n");
|
|
285
|
-
return { content: [{ type: "text", text }] };
|
|
286
434
|
}
|
|
287
|
-
|
|
288
|
-
.map((r) => `## ${r.category}/${r.filename} (relevance: ${r.score.toFixed(2)})\n**${r.title}**\n\n${r.snippet}\n`)
|
|
289
|
-
.join("\n---\n\n");
|
|
290
|
-
return { content: [{ type: "text", text }] };
|
|
435
|
+
return { content: [{ type: "text", text: text + getWriteNudge() }] };
|
|
291
436
|
}
|
|
292
437
|
case "context_write": {
|
|
293
|
-
const { category, filename, content, append = false } = args;
|
|
438
|
+
const { category, filename, content, append = false, supersedes } = args;
|
|
439
|
+
session.writeCallMade = true;
|
|
440
|
+
session.entriesWritten.push(`${category}/${filename}`);
|
|
294
441
|
// Validate category
|
|
295
442
|
if (!VALID_CATEGORIES.includes(category)) {
|
|
296
443
|
return {
|
|
@@ -304,6 +451,32 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
304
451
|
if (!store.exists()) {
|
|
305
452
|
store.scaffold();
|
|
306
453
|
}
|
|
454
|
+
// Auto-purge: handle explicit supersedes
|
|
455
|
+
let supersedesDeleted = false;
|
|
456
|
+
if (supersedes) {
|
|
457
|
+
const supersedeFname = supersedes.endsWith(".md") ? supersedes : supersedes + ".md";
|
|
458
|
+
supersedesDeleted = store.deleteEntry(category, supersedeFname);
|
|
459
|
+
if (supersedesDeleted && searchIndex) {
|
|
460
|
+
await searchIndex.removeEntry(category, supersedeFname);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Auto-purge: detect potentially overlapping entries
|
|
464
|
+
let supersededList = [];
|
|
465
|
+
if (!append && searchIndex) {
|
|
466
|
+
try {
|
|
467
|
+
const searchTerms = filename.replace(/-/g, " ");
|
|
468
|
+
const existing = await searchIndex.search(searchTerms, category, 3);
|
|
469
|
+
supersededList = existing
|
|
470
|
+
.filter((r) => r.category === category &&
|
|
471
|
+
r.filename !== filename + ".md" &&
|
|
472
|
+
r.filename !== filename &&
|
|
473
|
+
r.score > 2.0)
|
|
474
|
+
.map((d) => `${d.category}/${d.filename} (score: ${d.score.toFixed(1)})`);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// Best-effort overlap detection
|
|
478
|
+
}
|
|
479
|
+
}
|
|
307
480
|
let relativePath;
|
|
308
481
|
if (append) {
|
|
309
482
|
relativePath = store.appendEntry(category, filename, content);
|
|
@@ -319,15 +492,26 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
319
492
|
await searchIndex.indexEntry(entry);
|
|
320
493
|
}
|
|
321
494
|
}
|
|
495
|
+
let responseText = `\u2713 Written to ${relativePath}${append ? " (appended)" : ""}.`;
|
|
496
|
+
if (supersedes && supersedesDeleted) {
|
|
497
|
+
responseText += `\n\u2713 Superseded and deleted: ${category}/${supersedes}`;
|
|
498
|
+
}
|
|
499
|
+
else if (supersedes && !supersedesDeleted) {
|
|
500
|
+
responseText += `\n\u26a0 Could not find ${category}/${supersedes} to supersede (file not found).`;
|
|
501
|
+
}
|
|
502
|
+
if (supersededList.length > 0) {
|
|
503
|
+
responseText += `\n\u26a0 Potentially supersedes: ${supersededList.join(", ")}\n Consider deleting old entries with context_delete if they're now outdated.`;
|
|
504
|
+
}
|
|
322
505
|
return {
|
|
323
506
|
content: [{
|
|
324
507
|
type: "text",
|
|
325
|
-
text:
|
|
508
|
+
text: responseText,
|
|
326
509
|
}],
|
|
327
510
|
};
|
|
328
511
|
}
|
|
329
512
|
case "context_delete": {
|
|
330
513
|
const { category, filename } = args;
|
|
514
|
+
session.entriesDeleted.push(`${category}/${filename}`);
|
|
331
515
|
if (!VALID_CATEGORIES.includes(category)) {
|
|
332
516
|
return {
|
|
333
517
|
content: [{
|
|
@@ -359,7 +543,8 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
359
543
|
};
|
|
360
544
|
}
|
|
361
545
|
case "context_list": {
|
|
362
|
-
const { category } = (args || {});
|
|
546
|
+
const { category, compact = true } = (args || {});
|
|
547
|
+
session.readCallCount++;
|
|
363
548
|
if (category && !VALID_CATEGORIES.includes(category)) {
|
|
364
549
|
return {
|
|
365
550
|
content: [{
|
|
@@ -373,7 +558,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
373
558
|
return {
|
|
374
559
|
content: [{
|
|
375
560
|
type: "text",
|
|
376
|
-
text: "No .context/ directory found. Run `repomemory
|
|
561
|
+
text: "No .context/ directory found. Run `npx repomemory go` to set up.",
|
|
377
562
|
}],
|
|
378
563
|
};
|
|
379
564
|
}
|
|
@@ -382,7 +567,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
382
567
|
return {
|
|
383
568
|
content: [{
|
|
384
569
|
type: "text",
|
|
385
|
-
text: `No entries found${category ? ` in ${category}` : ""}. Run \`repomemory analyze\` to populate, or use context_write to add entries.`,
|
|
570
|
+
text: `No entries found${category ? ` in ${category}` : ""}. Run \`npx repomemory analyze\` to populate, or use context_write to add entries.`,
|
|
386
571
|
}],
|
|
387
572
|
};
|
|
388
573
|
}
|
|
@@ -392,20 +577,43 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
392
577
|
grouped[entry.category] = [];
|
|
393
578
|
grouped[entry.category].push(entry);
|
|
394
579
|
}
|
|
395
|
-
let text = "
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
580
|
+
let text = "";
|
|
581
|
+
if (compact) {
|
|
582
|
+
for (const [cat, catEntries] of Object.entries(grouped)) {
|
|
583
|
+
text += `**${cat}/** (${catEntries.length})\n`;
|
|
584
|
+
for (const entry of catEntries) {
|
|
585
|
+
const age = getRelativeTime(entry.lastModified);
|
|
586
|
+
text += `- ${entry.filename} \u2014 ${entry.title} (${age})\n`;
|
|
587
|
+
}
|
|
402
588
|
}
|
|
403
|
-
text += "\n";
|
|
404
589
|
}
|
|
405
|
-
|
|
590
|
+
else {
|
|
591
|
+
text = "# Repository Context\n\n";
|
|
592
|
+
for (const [cat, catEntries] of Object.entries(grouped)) {
|
|
593
|
+
text += `## ${cat}/\n`;
|
|
594
|
+
for (const entry of catEntries) {
|
|
595
|
+
const sizeKb = (entry.sizeBytes / 1024).toFixed(1);
|
|
596
|
+
const age = getRelativeTime(entry.lastModified);
|
|
597
|
+
text += `- **${entry.filename}** \u2014 ${entry.title} (${sizeKb}KB, ${age})\n`;
|
|
598
|
+
}
|
|
599
|
+
text += "\n";
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return { content: [{ type: "text", text: text.trimEnd() + getWriteNudge() }] };
|
|
406
603
|
}
|
|
407
604
|
case "context_read": {
|
|
408
605
|
const { category, filename } = args;
|
|
606
|
+
if (category && !VALID_CATEGORIES.includes(category)) {
|
|
607
|
+
return {
|
|
608
|
+
content: [{
|
|
609
|
+
type: "text",
|
|
610
|
+
text: `Invalid category: ${category}. Valid categories: ${VALID_CATEGORIES.join(", ")}`,
|
|
611
|
+
}],
|
|
612
|
+
isError: true,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
session.entriesRead.push(`${category}/${filename}`);
|
|
616
|
+
session.readCallCount++;
|
|
409
617
|
const fname = filename.endsWith(".md") ? filename : filename + ".md";
|
|
410
618
|
const content = store.readEntry(category, fname);
|
|
411
619
|
if (!content) {
|
|
@@ -423,6 +631,67 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
423
631
|
}],
|
|
424
632
|
};
|
|
425
633
|
}
|
|
634
|
+
case "context_auto_orient": {
|
|
635
|
+
if (!store.exists()) {
|
|
636
|
+
return {
|
|
637
|
+
content: [{
|
|
638
|
+
type: "text",
|
|
639
|
+
text: "No .context/ directory found. The user needs to run:\n\n npx repomemory go\n\nThis will set up persistent memory for this project.",
|
|
640
|
+
}],
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const parts = [];
|
|
644
|
+
// 1. Index.md content
|
|
645
|
+
const indexContent = store.readIndex();
|
|
646
|
+
if (indexContent && indexContent.trim().length > 0) {
|
|
647
|
+
parts.push("# Project Overview\n\n" + indexContent);
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
parts.push("# Project Overview\n\n*No index.md found. Run `npx repomemory analyze` to generate.*");
|
|
651
|
+
}
|
|
652
|
+
// 2. Developer preferences
|
|
653
|
+
const prefEntries = store.listEntries("preferences");
|
|
654
|
+
if (prefEntries.length > 0) {
|
|
655
|
+
parts.push("\n# Developer Preferences\n");
|
|
656
|
+
for (const p of prefEntries) {
|
|
657
|
+
parts.push(`**${p.title}**\n${p.content.slice(0, 300)}\n`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// 3. Recent session summaries (last 3)
|
|
661
|
+
const sessionEntries = store.listEntries("sessions");
|
|
662
|
+
const recentSessions = sessionEntries
|
|
663
|
+
.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime())
|
|
664
|
+
.slice(0, 3);
|
|
665
|
+
if (recentSessions.length > 0) {
|
|
666
|
+
parts.push("\n# Recent Sessions\n");
|
|
667
|
+
for (const s of recentSessions) {
|
|
668
|
+
const age = getRelativeTime(s.lastModified);
|
|
669
|
+
parts.push(`- **${s.title}** (${age}) \u2014 ${s.content.slice(0, 200).replace(/\n/g, " ")}...`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// 4. Recently modified entries (last 7 days, excluding sessions/changelog)
|
|
673
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
674
|
+
const allEntries = store.listEntries();
|
|
675
|
+
const recentEntries = allEntries
|
|
676
|
+
.filter((e) => e.category !== "sessions" &&
|
|
677
|
+
e.category !== "changelog" &&
|
|
678
|
+
e.category !== "root" &&
|
|
679
|
+
e.lastModified.getTime() > sevenDaysAgo)
|
|
680
|
+
.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime())
|
|
681
|
+
.slice(0, 10);
|
|
682
|
+
if (recentEntries.length > 0) {
|
|
683
|
+
parts.push("\n# Recently Updated\n");
|
|
684
|
+
for (const e of recentEntries) {
|
|
685
|
+
parts.push(`- ${e.category}/${e.filename}: ${e.title} (${getRelativeTime(e.lastModified)})`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// 5. Empty state warning
|
|
689
|
+
const stats = store.getStats();
|
|
690
|
+
if (stats.totalFiles === 0 || (stats.categories["facts"] || 0) === 0) {
|
|
691
|
+
parts.push("\n> **Note**: Context is mostly empty. Ask the user to run `npx repomemory analyze` to populate with architecture knowledge.");
|
|
692
|
+
}
|
|
693
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
694
|
+
}
|
|
426
695
|
default:
|
|
427
696
|
return {
|
|
428
697
|
content: [{
|
|
@@ -455,6 +724,10 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
455
724
|
throw new Error(`Invalid URI: ${uri}`);
|
|
456
725
|
}
|
|
457
726
|
const [, category, filename] = match;
|
|
727
|
+
// Validate category to prevent path traversal
|
|
728
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
729
|
+
throw new Error(`Invalid category in URI: ${category}`);
|
|
730
|
+
}
|
|
458
731
|
const content = store.readEntry(category, filename);
|
|
459
732
|
if (!content) {
|
|
460
733
|
throw new Error(`Resource not found: ${uri}`);
|
|
@@ -467,8 +740,25 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
467
740
|
}],
|
|
468
741
|
};
|
|
469
742
|
});
|
|
470
|
-
// Graceful shutdown
|
|
743
|
+
// --- Graceful shutdown with auto-session capture ---
|
|
744
|
+
let cleanupDone = false;
|
|
471
745
|
const cleanup = () => {
|
|
746
|
+
if (cleanupDone)
|
|
747
|
+
return;
|
|
748
|
+
cleanupDone = true;
|
|
749
|
+
// Auto-write session summary if there was meaningful activity
|
|
750
|
+
const duration = Math.round((Date.now() - session.startTime.getTime()) / 1000);
|
|
751
|
+
const hasActivity = session.toolCalls.length > 2;
|
|
752
|
+
if (hasActivity && store.exists()) {
|
|
753
|
+
try {
|
|
754
|
+
const date = new Date().toISOString().split("T")[0];
|
|
755
|
+
const summary = buildSessionSummary(session, duration);
|
|
756
|
+
store.appendEntry("sessions", `auto-${date}`, summary);
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
// Best-effort, don't fail shutdown
|
|
760
|
+
}
|
|
761
|
+
}
|
|
472
762
|
if (searchIndex) {
|
|
473
763
|
searchIndex.close();
|
|
474
764
|
searchIndex = null;
|
|
@@ -480,7 +770,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
480
770
|
const transport = new StdioServerTransport();
|
|
481
771
|
await server.connect(transport);
|
|
482
772
|
}
|
|
483
|
-
function getRelativeTime(date) {
|
|
773
|
+
export function getRelativeTime(date) {
|
|
484
774
|
const seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
|
|
485
775
|
if (seconds < 60)
|
|
486
776
|
return "just now";
|