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.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/README.md +93 -34
  3. package/dist/commands/analyze.d.ts.map +1 -1
  4. package/dist/commands/analyze.js +20 -0
  5. package/dist/commands/analyze.js.map +1 -1
  6. package/dist/commands/dashboard.d.ts.map +1 -1
  7. package/dist/commands/dashboard.js +282 -36
  8. package/dist/commands/dashboard.js.map +1 -1
  9. package/dist/commands/go.d.ts +6 -0
  10. package/dist/commands/go.d.ts.map +1 -0
  11. package/dist/commands/go.js +165 -0
  12. package/dist/commands/go.js.map +1 -0
  13. package/dist/commands/init.d.ts +1 -0
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +42 -24
  16. package/dist/commands/init.js.map +1 -1
  17. package/dist/commands/setup.js +7 -1
  18. package/dist/commands/setup.js.map +1 -1
  19. package/dist/commands/status.d.ts.map +1 -1
  20. package/dist/commands/status.js +3 -0
  21. package/dist/commands/status.js.map +1 -1
  22. package/dist/commands/wizard.d.ts.map +1 -1
  23. package/dist/commands/wizard.js +2 -0
  24. package/dist/commands/wizard.js.map +1 -1
  25. package/dist/index.js +8 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/config.d.ts +9 -0
  28. package/dist/lib/config.d.ts.map +1 -1
  29. package/dist/lib/config.js +8 -1
  30. package/dist/lib/config.js.map +1 -1
  31. package/dist/lib/context-store.d.ts.map +1 -1
  32. package/dist/lib/context-store.js +2 -1
  33. package/dist/lib/context-store.js.map +1 -1
  34. package/dist/lib/embeddings.d.ts +27 -0
  35. package/dist/lib/embeddings.d.ts.map +1 -0
  36. package/dist/lib/embeddings.js +100 -0
  37. package/dist/lib/embeddings.js.map +1 -0
  38. package/dist/lib/search.d.ts +9 -1
  39. package/dist/lib/search.d.ts.map +1 -1
  40. package/dist/lib/search.js +208 -16
  41. package/dist/lib/search.js.map +1 -1
  42. package/dist/mcp/server.d.ts +26 -0
  43. package/dist/mcp/server.d.ts.map +1 -1
  44. package/dist/mcp/server.js +322 -43
  45. package/dist/mcp/server.js.map +1 -1
  46. package/package.json +1 -1
  47. package/server.json +11 -7
  48. package/skills/repomemory/SKILL.md +7 -4
  49. package/skills/session-end/SKILL.md +19 -0
  50. package/skills/session-start/SKILL.md +14 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAI1D,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,IAAI,CAAC,CAujBf"}
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"}
@@ -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
- const VALID_CATEGORIES = ["facts", "decisions", "regressions", "sessions", "changelog"];
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.0.0",
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\nPlease search the repository's persistent knowledge base for any relevant context architecture docs, past decisions, known regressions, or session notes that would help me be productive immediately. Use the context_search tool with relevant queries.`,
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 in the repository's persistent memory using context_write with category "sessions" so future AI sessions can benefit from what we learned today.`,
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 write anything you'd want a future version of yourself to know.",
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 prune aggressively.",
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. Run `repomemory init && repomemory analyze` to set up.",
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
- const results = await searchIndex.search(query, category, limit);
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
- const text = matched
283
- .map((e) => `## ${e.category}/${e.filename}\n**${e.title}**\n\n${e.content.slice(0, 800)}\n`)
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
- const text = results
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: `\u2713 Written to ${relativePath}${append ? " (appended)" : ""}. This knowledge will persist across sessions.`,
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 init` first.",
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 = "# Repository Context\n\n";
396
- for (const [cat, catEntries] of Object.entries(grouped)) {
397
- text += `## ${cat}/\n`;
398
- for (const entry of catEntries) {
399
- const sizeKb = (entry.sizeBytes / 1024).toFixed(1);
400
- const age = getRelativeTime(entry.lastModified);
401
- text += `- **${entry.filename}** \u2014 ${entry.title} (${sizeKb}KB, ${age})\n`;
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
- return { content: [{ type: "text", text }] };
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";