repomemory 1.0.2 → 1.1.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/.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 +282 -36
- 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 +165 -0
- package/dist/commands/go.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +42 -24
- package/dist/commands/init.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 +2 -0
- 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 +2 -1
- 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 +100 -0
- package/dist/lib/embeddings.js.map +1 -0
- package/dist/lib/search.d.ts +9 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +208 -16
- 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 +322 -43
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
- package/server.json +11 -7
- 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.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAM1D,UAAU,cAAc;IACtB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE,EAAE,CAAC;IAC/C,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAeD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CA0B5F;AAID;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAmBrE;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,IAAI,CAAC,CA+yBf;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAOlD"}
|
package/dist/mcp/server.js
CHANGED
|
@@ -3,22 +3,97 @@ 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
|
+
const VALID_CATEGORIES = ["facts", "decisions", "regressions", "sessions", "changelog", "preferences"];
|
|
8
|
+
function createSessionTracker() {
|
|
9
|
+
return {
|
|
10
|
+
startTime: new Date(),
|
|
11
|
+
toolCalls: [],
|
|
12
|
+
searchQueries: [],
|
|
13
|
+
entriesRead: [],
|
|
14
|
+
entriesWritten: [],
|
|
15
|
+
entriesDeleted: [],
|
|
16
|
+
writeCallMade: false,
|
|
17
|
+
readCallCount: 0,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function buildSessionSummary(session, durationSeconds) {
|
|
21
|
+
const date = new Date().toISOString().split("T")[0];
|
|
22
|
+
const mins = Math.round(durationSeconds / 60);
|
|
23
|
+
const parts = [];
|
|
24
|
+
parts.push(`## Auto-captured session ${date} (${mins}min)\n`);
|
|
25
|
+
if (session.searchQueries.length > 0) {
|
|
26
|
+
parts.push(`**Searched:** ${[...new Set(session.searchQueries)].join(", ")}`);
|
|
27
|
+
}
|
|
28
|
+
if (session.entriesRead.length > 0) {
|
|
29
|
+
parts.push(`**Read:** ${[...new Set(session.entriesRead)].join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
if (session.entriesWritten.length > 0) {
|
|
32
|
+
parts.push(`**Written:** ${[...new Set(session.entriesWritten)].join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
if (session.entriesDeleted.length > 0) {
|
|
35
|
+
parts.push(`**Deleted:** ${[...new Set(session.entriesDeleted)].join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
parts.push(`**Total tool calls:** ${session.toolCalls.length}`);
|
|
38
|
+
return parts.join("\n");
|
|
39
|
+
}
|
|
40
|
+
// --- Intelligent Category Routing ---
|
|
41
|
+
/**
|
|
42
|
+
* Detect the most likely category for a search query using keyword heuristics.
|
|
43
|
+
* Returns undefined if no category can be confidently inferred.
|
|
44
|
+
*
|
|
45
|
+
* Precedence order is intentional: decisions > regressions > preferences > sessions > facts.
|
|
46
|
+
* For ambiguous queries (e.g., "why did the login crash"), decisions wins because
|
|
47
|
+
* understanding the "why" is usually more actionable. The caller retries without
|
|
48
|
+
* category filter if the routed search returns 0 results.
|
|
49
|
+
*/
|
|
50
|
+
export function detectQueryCategory(query) {
|
|
51
|
+
const q = query.toLowerCase();
|
|
52
|
+
// Decision-related queries — "why" is the strongest signal
|
|
53
|
+
if (/\b(why\b|chose|decision|alternatives?|trade.?off|instead of|reason\b)/.test(q))
|
|
54
|
+
return "decisions";
|
|
55
|
+
// Regression/bug queries
|
|
56
|
+
if (/\b(bug|broke|regression|crash|error\b|fail|fix\b|issues?\b|problem|broken)/.test(q))
|
|
57
|
+
return "regressions";
|
|
58
|
+
// Preference/style queries
|
|
59
|
+
if (/\b(prefer|style|convention|format|pattern|coding style|tab|indent|lint)/.test(q))
|
|
60
|
+
return "preferences";
|
|
61
|
+
// Session queries
|
|
62
|
+
if (/\b(last session|previous session|yesterday|worked on|accomplished)/.test(q))
|
|
63
|
+
return "sessions";
|
|
64
|
+
// Architecture/fact queries
|
|
65
|
+
if (/\b(how does|architecture|schema|database|api|endpoint|flow|structure)/.test(q))
|
|
66
|
+
return "facts";
|
|
67
|
+
return undefined; // search all categories
|
|
68
|
+
}
|
|
7
69
|
export async function startMcpServer(repoRoot, config) {
|
|
8
70
|
const store = new ContextStore(repoRoot, config);
|
|
9
71
|
let searchIndex = null;
|
|
72
|
+
// Initialize embedding provider (optional — falls back to keyword search)
|
|
73
|
+
let embeddingProvider = null;
|
|
74
|
+
try {
|
|
75
|
+
embeddingProvider = await createEmbeddingProvider({
|
|
76
|
+
provider: config.embeddingProvider,
|
|
77
|
+
model: config.embeddingModel,
|
|
78
|
+
apiKey: config.embeddingApiKey,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// No embeddings available, will use keyword-only search
|
|
83
|
+
}
|
|
10
84
|
if (store.exists()) {
|
|
11
85
|
try {
|
|
12
|
-
searchIndex = new SearchIndex(store.path, store);
|
|
86
|
+
searchIndex = new SearchIndex(store.path, store, embeddingProvider, config.hybridAlpha);
|
|
13
87
|
await searchIndex.rebuild();
|
|
14
88
|
}
|
|
15
89
|
catch (e) {
|
|
16
90
|
console.error("Warning: Could not initialize search index:", e);
|
|
17
91
|
}
|
|
18
92
|
}
|
|
93
|
+
const session = createSessionTracker();
|
|
19
94
|
const server = new Server({
|
|
20
95
|
name: "repomemory",
|
|
21
|
-
version: "1.
|
|
96
|
+
version: "1.1.0",
|
|
22
97
|
}, {
|
|
23
98
|
capabilities: {
|
|
24
99
|
tools: {},
|
|
@@ -42,7 +117,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
42
117
|
},
|
|
43
118
|
{
|
|
44
119
|
name: "end-session",
|
|
45
|
-
description: "Record what you accomplished and discovered during this session.",
|
|
120
|
+
description: "Record what you accomplished and discovered during this session. Routes conclusions to the right categories.",
|
|
46
121
|
arguments: [
|
|
47
122
|
{
|
|
48
123
|
name: "summary",
|
|
@@ -64,7 +139,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
64
139
|
role: "user",
|
|
65
140
|
content: {
|
|
66
141
|
type: "text",
|
|
67
|
-
text: `I'm about to work on: ${task}\n\
|
|
142
|
+
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
143
|
},
|
|
69
144
|
},
|
|
70
145
|
],
|
|
@@ -79,7 +154,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
79
154
|
role: "user",
|
|
80
155
|
content: {
|
|
81
156
|
type: "text",
|
|
82
|
-
text: `Session summary: ${summary}\n\nPlease record this
|
|
157
|
+
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
158
|
},
|
|
84
159
|
},
|
|
85
160
|
],
|
|
@@ -110,6 +185,11 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
110
185
|
type: "number",
|
|
111
186
|
description: "Max results to return (default: 5)",
|
|
112
187
|
},
|
|
188
|
+
detail: {
|
|
189
|
+
type: "string",
|
|
190
|
+
enum: ["compact", "full"],
|
|
191
|
+
description: "Level of detail. 'compact' (default) returns one-line summaries (~50 tokens each). 'full' returns longer snippets.",
|
|
192
|
+
},
|
|
113
193
|
},
|
|
114
194
|
required: ["query"],
|
|
115
195
|
},
|
|
@@ -123,19 +203,14 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
123
203
|
},
|
|
124
204
|
{
|
|
125
205
|
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
|
|
206
|
+
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
207
|
inputSchema: {
|
|
128
208
|
type: "object",
|
|
129
209
|
properties: {
|
|
130
210
|
category: {
|
|
131
211
|
type: "string",
|
|
132
212
|
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`,
|
|
213
|
+
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
214
|
},
|
|
140
215
|
filename: {
|
|
141
216
|
type: "string",
|
|
@@ -149,6 +224,10 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
149
224
|
type: "boolean",
|
|
150
225
|
description: "If true, append to existing file instead of overwriting. Useful for session logs.",
|
|
151
226
|
},
|
|
227
|
+
supersedes: {
|
|
228
|
+
type: "string",
|
|
229
|
+
description: "Filename of an existing entry in the same category that this replaces. The old entry will be auto-deleted.",
|
|
230
|
+
},
|
|
152
231
|
},
|
|
153
232
|
required: ["category", "filename", "content"],
|
|
154
233
|
},
|
|
@@ -162,7 +241,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
162
241
|
},
|
|
163
242
|
{
|
|
164
243
|
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
|
|
244
|
+
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
245
|
inputSchema: {
|
|
167
246
|
type: "object",
|
|
168
247
|
properties: {
|
|
@@ -197,6 +276,10 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
197
276
|
enum: VALID_CATEGORIES,
|
|
198
277
|
description: "Optional: filter to a specific category.",
|
|
199
278
|
},
|
|
279
|
+
compact: {
|
|
280
|
+
type: "boolean",
|
|
281
|
+
description: "If true (default), returns one-line summaries. If false, includes file sizes.",
|
|
282
|
+
},
|
|
200
283
|
},
|
|
201
284
|
},
|
|
202
285
|
annotations: {
|
|
@@ -215,7 +298,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
215
298
|
properties: {
|
|
216
299
|
category: {
|
|
217
300
|
type: "string",
|
|
218
|
-
description: "The category (facts, decisions, regressions, sessions, changelog)",
|
|
301
|
+
description: "The category (facts, decisions, regressions, sessions, changelog, preferences)",
|
|
219
302
|
},
|
|
220
303
|
filename: {
|
|
221
304
|
type: "string",
|
|
@@ -232,14 +315,40 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
232
315
|
openWorldHint: false,
|
|
233
316
|
},
|
|
234
317
|
},
|
|
318
|
+
{
|
|
319
|
+
name: "context_auto_orient",
|
|
320
|
+
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.",
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: {},
|
|
324
|
+
},
|
|
325
|
+
annotations: {
|
|
326
|
+
title: "Auto Orient",
|
|
327
|
+
readOnlyHint: true,
|
|
328
|
+
destructiveHint: false,
|
|
329
|
+
idempotentHint: true,
|
|
330
|
+
openWorldHint: false,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
235
333
|
],
|
|
236
334
|
};
|
|
237
335
|
});
|
|
238
336
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
239
337
|
const { name, arguments: args } = request.params;
|
|
338
|
+
// Track every tool call for session capture
|
|
339
|
+
session.toolCalls.push({ tool: name, timestamp: new Date() });
|
|
340
|
+
// Build write-nudge suffix for non-writing sessions
|
|
341
|
+
const getWriteNudge = () => {
|
|
342
|
+
if (!session.writeCallMade && session.readCallCount >= 3) {
|
|
343
|
+
return "\n\n> Tip: Use `context_write` to record any discoveries or decisions from this session.";
|
|
344
|
+
}
|
|
345
|
+
return "";
|
|
346
|
+
};
|
|
240
347
|
switch (name) {
|
|
241
348
|
case "context_search": {
|
|
242
|
-
const { query, category, limit = 5 } = args;
|
|
349
|
+
const { query, category, limit = 5, detail = "compact" } = args;
|
|
350
|
+
session.searchQueries.push(query);
|
|
351
|
+
session.readCallCount++;
|
|
243
352
|
// Validate category if provided
|
|
244
353
|
if (category && !VALID_CATEGORIES.includes(category)) {
|
|
245
354
|
return {
|
|
@@ -254,7 +363,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
254
363
|
return {
|
|
255
364
|
content: [{
|
|
256
365
|
type: "text",
|
|
257
|
-
text: "No .context/ directory found.
|
|
366
|
+
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
367
|
}],
|
|
259
368
|
};
|
|
260
369
|
}
|
|
@@ -262,9 +371,24 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
262
371
|
searchIndex = new SearchIndex(store.path, store);
|
|
263
372
|
await searchIndex.rebuild();
|
|
264
373
|
}
|
|
265
|
-
|
|
374
|
+
// Intelligent category routing: auto-detect if not explicitly provided
|
|
375
|
+
let effectiveCategory = category;
|
|
376
|
+
let routingNote = "";
|
|
377
|
+
if (!category) {
|
|
378
|
+
const detected = detectQueryCategory(query);
|
|
379
|
+
if (detected) {
|
|
380
|
+
effectiveCategory = detected;
|
|
381
|
+
routingNote = `(auto-routed to ${detected}/) `;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
let results = await searchIndex.search(query, effectiveCategory, limit);
|
|
385
|
+
// If routing returned 0 results, retry without category filter
|
|
386
|
+
if (results.length === 0 && effectiveCategory && !category) {
|
|
387
|
+
results = await searchIndex.search(query, undefined, limit);
|
|
388
|
+
routingNote = "";
|
|
389
|
+
}
|
|
266
390
|
if (results.length === 0) {
|
|
267
|
-
// Fallback to simple text search
|
|
391
|
+
// Fallback to simple text search (use explicit category, not auto-routed)
|
|
268
392
|
const entries = store.listEntries(category);
|
|
269
393
|
const queryLower = query.toLowerCase();
|
|
270
394
|
const matched = entries
|
|
@@ -275,22 +399,42 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
275
399
|
return {
|
|
276
400
|
content: [{
|
|
277
401
|
type: "text",
|
|
278
|
-
text: `No results found for "${query}"${category ? ` in ${category}` : ""}. Try a different query or browse with context_list
|
|
402
|
+
text: `No results found for "${query}"${category ? ` in ${category}` : ""}. Try a different query or browse with context_list.${getWriteNudge()}`,
|
|
279
403
|
}],
|
|
280
404
|
};
|
|
281
405
|
}
|
|
282
|
-
|
|
283
|
-
|
|
406
|
+
// Format fallback results respecting detail level
|
|
407
|
+
let text;
|
|
408
|
+
if (detail === "compact") {
|
|
409
|
+
text = routingNote + matched
|
|
410
|
+
.map((e) => `- **${e.title}** [${e.category}/${e.filename}] \u2014 ${e.content.slice(0, 150).replace(/\n/g, " ")}...`)
|
|
411
|
+
.join("\n");
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
text = routingNote + matched
|
|
415
|
+
.map((e) => `## ${e.category}/${e.filename}\n**${e.title}**\n\n${e.content.slice(0, 800)}\n`)
|
|
416
|
+
.join("\n---\n\n");
|
|
417
|
+
}
|
|
418
|
+
return { content: [{ type: "text", text: text + getWriteNudge() }] };
|
|
419
|
+
}
|
|
420
|
+
// Format search results based on detail level
|
|
421
|
+
let text;
|
|
422
|
+
if (detail === "compact") {
|
|
423
|
+
text = routingNote + results
|
|
424
|
+
.map((r) => `- **${r.title}** [${r.category}/${r.filename}] (score: ${r.score.toFixed(2)}) \u2014 ${r.snippet.slice(0, 150).replace(/\n/g, " ")}...`)
|
|
425
|
+
.join("\n");
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
text = routingNote + results
|
|
429
|
+
.map((r) => `## ${r.category}/${r.filename} (relevance: ${r.score.toFixed(2)})\n**${r.title}**\n\n${r.snippet}\n`)
|
|
284
430
|
.join("\n---\n\n");
|
|
285
|
-
return { content: [{ type: "text", text }] };
|
|
286
431
|
}
|
|
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 }] };
|
|
432
|
+
return { content: [{ type: "text", text: text + getWriteNudge() }] };
|
|
291
433
|
}
|
|
292
434
|
case "context_write": {
|
|
293
|
-
const { category, filename, content, append = false } = args;
|
|
435
|
+
const { category, filename, content, append = false, supersedes } = args;
|
|
436
|
+
session.writeCallMade = true;
|
|
437
|
+
session.entriesWritten.push(`${category}/${filename}`);
|
|
294
438
|
// Validate category
|
|
295
439
|
if (!VALID_CATEGORIES.includes(category)) {
|
|
296
440
|
return {
|
|
@@ -304,6 +448,32 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
304
448
|
if (!store.exists()) {
|
|
305
449
|
store.scaffold();
|
|
306
450
|
}
|
|
451
|
+
// Auto-purge: handle explicit supersedes
|
|
452
|
+
let supersedesDeleted = false;
|
|
453
|
+
if (supersedes) {
|
|
454
|
+
const supersedeFname = supersedes.endsWith(".md") ? supersedes : supersedes + ".md";
|
|
455
|
+
supersedesDeleted = store.deleteEntry(category, supersedeFname);
|
|
456
|
+
if (supersedesDeleted && searchIndex) {
|
|
457
|
+
await searchIndex.removeEntry(category, supersedeFname);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Auto-purge: detect potentially overlapping entries
|
|
461
|
+
let supersededList = [];
|
|
462
|
+
if (!append && searchIndex) {
|
|
463
|
+
try {
|
|
464
|
+
const searchTerms = filename.replace(/-/g, " ");
|
|
465
|
+
const existing = await searchIndex.search(searchTerms, category, 3);
|
|
466
|
+
supersededList = existing
|
|
467
|
+
.filter((r) => r.category === category &&
|
|
468
|
+
r.filename !== filename + ".md" &&
|
|
469
|
+
r.filename !== filename &&
|
|
470
|
+
r.score > 2.0)
|
|
471
|
+
.map((d) => `${d.category}/${d.filename} (score: ${d.score.toFixed(1)})`);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
// Best-effort overlap detection
|
|
475
|
+
}
|
|
476
|
+
}
|
|
307
477
|
let relativePath;
|
|
308
478
|
if (append) {
|
|
309
479
|
relativePath = store.appendEntry(category, filename, content);
|
|
@@ -319,15 +489,26 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
319
489
|
await searchIndex.indexEntry(entry);
|
|
320
490
|
}
|
|
321
491
|
}
|
|
492
|
+
let responseText = `\u2713 Written to ${relativePath}${append ? " (appended)" : ""}.`;
|
|
493
|
+
if (supersedes && supersedesDeleted) {
|
|
494
|
+
responseText += `\n\u2713 Superseded and deleted: ${category}/${supersedes}`;
|
|
495
|
+
}
|
|
496
|
+
else if (supersedes && !supersedesDeleted) {
|
|
497
|
+
responseText += `\n\u26a0 Could not find ${category}/${supersedes} to supersede (file not found).`;
|
|
498
|
+
}
|
|
499
|
+
if (supersededList.length > 0) {
|
|
500
|
+
responseText += `\n\u26a0 Potentially supersedes: ${supersededList.join(", ")}\n Consider deleting old entries with context_delete if they're now outdated.`;
|
|
501
|
+
}
|
|
322
502
|
return {
|
|
323
503
|
content: [{
|
|
324
504
|
type: "text",
|
|
325
|
-
text:
|
|
505
|
+
text: responseText,
|
|
326
506
|
}],
|
|
327
507
|
};
|
|
328
508
|
}
|
|
329
509
|
case "context_delete": {
|
|
330
510
|
const { category, filename } = args;
|
|
511
|
+
session.entriesDeleted.push(`${category}/${filename}`);
|
|
331
512
|
if (!VALID_CATEGORIES.includes(category)) {
|
|
332
513
|
return {
|
|
333
514
|
content: [{
|
|
@@ -359,7 +540,8 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
359
540
|
};
|
|
360
541
|
}
|
|
361
542
|
case "context_list": {
|
|
362
|
-
const { category } = (args || {});
|
|
543
|
+
const { category, compact = true } = (args || {});
|
|
544
|
+
session.readCallCount++;
|
|
363
545
|
if (category && !VALID_CATEGORIES.includes(category)) {
|
|
364
546
|
return {
|
|
365
547
|
content: [{
|
|
@@ -373,7 +555,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
373
555
|
return {
|
|
374
556
|
content: [{
|
|
375
557
|
type: "text",
|
|
376
|
-
text: "No .context/ directory found. Run `repomemory
|
|
558
|
+
text: "No .context/ directory found. Run `npx repomemory go` to set up.",
|
|
377
559
|
}],
|
|
378
560
|
};
|
|
379
561
|
}
|
|
@@ -382,7 +564,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
382
564
|
return {
|
|
383
565
|
content: [{
|
|
384
566
|
type: "text",
|
|
385
|
-
text: `No entries found${category ? ` in ${category}` : ""}. Run \`repomemory analyze\` to populate, or use context_write to add entries.`,
|
|
567
|
+
text: `No entries found${category ? ` in ${category}` : ""}. Run \`npx repomemory analyze\` to populate, or use context_write to add entries.`,
|
|
386
568
|
}],
|
|
387
569
|
};
|
|
388
570
|
}
|
|
@@ -392,20 +574,43 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
392
574
|
grouped[entry.category] = [];
|
|
393
575
|
grouped[entry.category].push(entry);
|
|
394
576
|
}
|
|
395
|
-
let text = "
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
577
|
+
let text = "";
|
|
578
|
+
if (compact) {
|
|
579
|
+
for (const [cat, catEntries] of Object.entries(grouped)) {
|
|
580
|
+
text += `**${cat}/** (${catEntries.length})\n`;
|
|
581
|
+
for (const entry of catEntries) {
|
|
582
|
+
const age = getRelativeTime(entry.lastModified);
|
|
583
|
+
text += `- ${entry.filename} \u2014 ${entry.title} (${age})\n`;
|
|
584
|
+
}
|
|
402
585
|
}
|
|
403
|
-
text += "\n";
|
|
404
586
|
}
|
|
405
|
-
|
|
587
|
+
else {
|
|
588
|
+
text = "# Repository Context\n\n";
|
|
589
|
+
for (const [cat, catEntries] of Object.entries(grouped)) {
|
|
590
|
+
text += `## ${cat}/\n`;
|
|
591
|
+
for (const entry of catEntries) {
|
|
592
|
+
const sizeKb = (entry.sizeBytes / 1024).toFixed(1);
|
|
593
|
+
const age = getRelativeTime(entry.lastModified);
|
|
594
|
+
text += `- **${entry.filename}** \u2014 ${entry.title} (${sizeKb}KB, ${age})\n`;
|
|
595
|
+
}
|
|
596
|
+
text += "\n";
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return { content: [{ type: "text", text: text.trimEnd() + getWriteNudge() }] };
|
|
406
600
|
}
|
|
407
601
|
case "context_read": {
|
|
408
602
|
const { category, filename } = args;
|
|
603
|
+
if (category && !VALID_CATEGORIES.includes(category)) {
|
|
604
|
+
return {
|
|
605
|
+
content: [{
|
|
606
|
+
type: "text",
|
|
607
|
+
text: `Invalid category: ${category}. Valid categories: ${VALID_CATEGORIES.join(", ")}`,
|
|
608
|
+
}],
|
|
609
|
+
isError: true,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
session.entriesRead.push(`${category}/${filename}`);
|
|
613
|
+
session.readCallCount++;
|
|
409
614
|
const fname = filename.endsWith(".md") ? filename : filename + ".md";
|
|
410
615
|
const content = store.readEntry(category, fname);
|
|
411
616
|
if (!content) {
|
|
@@ -423,6 +628,67 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
423
628
|
}],
|
|
424
629
|
};
|
|
425
630
|
}
|
|
631
|
+
case "context_auto_orient": {
|
|
632
|
+
if (!store.exists()) {
|
|
633
|
+
return {
|
|
634
|
+
content: [{
|
|
635
|
+
type: "text",
|
|
636
|
+
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.",
|
|
637
|
+
}],
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const parts = [];
|
|
641
|
+
// 1. Index.md content
|
|
642
|
+
const indexContent = store.readIndex();
|
|
643
|
+
if (indexContent && indexContent.trim().length > 0) {
|
|
644
|
+
parts.push("# Project Overview\n\n" + indexContent);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
parts.push("# Project Overview\n\n*No index.md found. Run `npx repomemory analyze` to generate.*");
|
|
648
|
+
}
|
|
649
|
+
// 2. Developer preferences
|
|
650
|
+
const prefEntries = store.listEntries("preferences");
|
|
651
|
+
if (prefEntries.length > 0) {
|
|
652
|
+
parts.push("\n# Developer Preferences\n");
|
|
653
|
+
for (const p of prefEntries) {
|
|
654
|
+
parts.push(`**${p.title}**\n${p.content.slice(0, 300)}\n`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// 3. Recent session summaries (last 3)
|
|
658
|
+
const sessionEntries = store.listEntries("sessions");
|
|
659
|
+
const recentSessions = sessionEntries
|
|
660
|
+
.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime())
|
|
661
|
+
.slice(0, 3);
|
|
662
|
+
if (recentSessions.length > 0) {
|
|
663
|
+
parts.push("\n# Recent Sessions\n");
|
|
664
|
+
for (const s of recentSessions) {
|
|
665
|
+
const age = getRelativeTime(s.lastModified);
|
|
666
|
+
parts.push(`- **${s.title}** (${age}) \u2014 ${s.content.slice(0, 200).replace(/\n/g, " ")}...`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// 4. Recently modified entries (last 7 days, excluding sessions/changelog)
|
|
670
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
671
|
+
const allEntries = store.listEntries();
|
|
672
|
+
const recentEntries = allEntries
|
|
673
|
+
.filter((e) => e.category !== "sessions" &&
|
|
674
|
+
e.category !== "changelog" &&
|
|
675
|
+
e.category !== "root" &&
|
|
676
|
+
e.lastModified.getTime() > sevenDaysAgo)
|
|
677
|
+
.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime())
|
|
678
|
+
.slice(0, 10);
|
|
679
|
+
if (recentEntries.length > 0) {
|
|
680
|
+
parts.push("\n# Recently Updated\n");
|
|
681
|
+
for (const e of recentEntries) {
|
|
682
|
+
parts.push(`- ${e.category}/${e.filename}: ${e.title} (${getRelativeTime(e.lastModified)})`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// 5. Empty state warning
|
|
686
|
+
const stats = store.getStats();
|
|
687
|
+
if (stats.totalFiles === 0 || (stats.categories["facts"] || 0) === 0) {
|
|
688
|
+
parts.push("\n> **Note**: Context is mostly empty. Ask the user to run `npx repomemory analyze` to populate with architecture knowledge.");
|
|
689
|
+
}
|
|
690
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
691
|
+
}
|
|
426
692
|
default:
|
|
427
693
|
return {
|
|
428
694
|
content: [{
|
|
@@ -467,8 +733,21 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
467
733
|
}],
|
|
468
734
|
};
|
|
469
735
|
});
|
|
470
|
-
// Graceful shutdown
|
|
736
|
+
// --- Graceful shutdown with auto-session capture ---
|
|
471
737
|
const cleanup = () => {
|
|
738
|
+
// Auto-write session summary if there was meaningful activity
|
|
739
|
+
const duration = Math.round((Date.now() - session.startTime.getTime()) / 1000);
|
|
740
|
+
const hasActivity = session.toolCalls.length > 2;
|
|
741
|
+
if (hasActivity && store.exists()) {
|
|
742
|
+
try {
|
|
743
|
+
const date = new Date().toISOString().split("T")[0];
|
|
744
|
+
const summary = buildSessionSummary(session, duration);
|
|
745
|
+
store.appendEntry("sessions", `auto-${date}`, summary);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
// Best-effort, don't fail shutdown
|
|
749
|
+
}
|
|
750
|
+
}
|
|
472
751
|
if (searchIndex) {
|
|
473
752
|
searchIndex.close();
|
|
474
753
|
searchIndex = null;
|
|
@@ -480,7 +759,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
480
759
|
const transport = new StdioServerTransport();
|
|
481
760
|
await server.connect(transport);
|
|
482
761
|
}
|
|
483
|
-
function getRelativeTime(date) {
|
|
762
|
+
export function getRelativeTime(date) {
|
|
484
763
|
const seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
|
|
485
764
|
if (seconds < 60)
|
|
486
765
|
return "just now";
|