@contextmirror/claude-memory 0.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.
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Claude Memory MCP Server
3
+ *
4
+ * Exposes tools for Claude Code to query cross-project context:
5
+ * - get_global_context: Overview of all projects
6
+ * - get_project_summary: Details about a specific project
7
+ * - search_projects: Search across all projects
8
+ * - record_insight: Save a cross-project insight
9
+ */
10
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
+ import { readFileSync, existsSync, readdirSync, statSync, writeFileSync } from 'fs';
14
+ import { join } from 'path';
15
+ import { homedir } from 'os';
16
+ const MEMORY_DIR = join(homedir(), '.claude-memory');
17
+ // Tool definitions
18
+ const tools = [
19
+ {
20
+ name: 'get_global_context',
21
+ description: 'Get an overview of all projects the user is working on. Returns project names, descriptions, tech stacks, and recent activity. Use this at the start of conversations to understand the user\'s development environment.',
22
+ inputSchema: {
23
+ type: 'object',
24
+ properties: {},
25
+ required: [],
26
+ },
27
+ },
28
+ {
29
+ name: 'get_project_summary',
30
+ description: 'Get detailed information about a specific project. Includes description, tech stack, current branch, and any recorded insights.',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {
34
+ project: {
35
+ type: 'string',
36
+ description: 'Project name or partial path',
37
+ },
38
+ },
39
+ required: ['project'],
40
+ },
41
+ },
42
+ {
43
+ name: 'search_projects',
44
+ description: 'Search across all projects for mentions of a term. Useful for finding where patterns are implemented or features exist.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ query: {
49
+ type: 'string',
50
+ description: 'Search term (searches names, descriptions, tech stacks)',
51
+ },
52
+ },
53
+ required: ['query'],
54
+ },
55
+ },
56
+ {
57
+ name: 'record_insight',
58
+ description: 'Record a cross-project insight or pattern for future reference. Use this when you discover something that could be useful in other projects.',
59
+ inputSchema: {
60
+ type: 'object',
61
+ properties: {
62
+ content: {
63
+ type: 'string',
64
+ description: 'The insight to record',
65
+ },
66
+ relatedProjects: {
67
+ type: 'array',
68
+ items: { type: 'string' },
69
+ description: 'Names of related projects',
70
+ },
71
+ },
72
+ required: ['content'],
73
+ },
74
+ },
75
+ {
76
+ name: 'get_project_analysis',
77
+ description: 'Get deep analysis of a project for CLAUDE.md generation. Returns directory structure, README content, package.json details, and key file locations. Use this when the user asks you to generate or write a CLAUDE.md for a project.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ project: {
82
+ type: 'string',
83
+ description: 'Project name or path',
84
+ },
85
+ },
86
+ required: ['project'],
87
+ },
88
+ },
89
+ ];
90
+ function loadContext() {
91
+ const contextPath = join(MEMORY_DIR, 'context.json');
92
+ if (!existsSync(contextPath)) {
93
+ return null;
94
+ }
95
+ try {
96
+ return JSON.parse(readFileSync(contextPath, 'utf-8'));
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ function handleGetGlobalContext() {
103
+ const context = loadContext();
104
+ if (!context) {
105
+ return 'No projects scanned yet. Run `claude-memory scan` first.';
106
+ }
107
+ const lines = [
108
+ `# Your Development Environment`,
109
+ '',
110
+ `Last scanned: ${new Date(context.lastUpdated).toLocaleString()}`,
111
+ '',
112
+ '## Active Projects',
113
+ '',
114
+ ];
115
+ // Sort by activity
116
+ const sorted = [...context.projects].sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
117
+ for (const p of sorted) {
118
+ const dirty = p.isDirty ? ' ⚠️' : '';
119
+ lines.push(`### ${p.name}${dirty}`);
120
+ lines.push(`- **Path:** ${p.path}`);
121
+ lines.push(`- **Stack:** ${p.techStack.join(', ')}`);
122
+ lines.push(`- **Branch:** ${p.currentBranch}`);
123
+ lines.push(`- **Description:** ${p.description}`);
124
+ lines.push('');
125
+ }
126
+ return lines.join('\n');
127
+ }
128
+ function handleGetProjectSummary(projectQuery) {
129
+ const context = loadContext();
130
+ if (!context) {
131
+ return 'No projects scanned yet. Run `claude-memory scan` first.';
132
+ }
133
+ const project = context.projects.find((p) => p.name.toLowerCase() === projectQuery.toLowerCase() ||
134
+ p.path.toLowerCase().includes(projectQuery.toLowerCase()));
135
+ if (!project) {
136
+ const names = context.projects.map((p) => p.name).join(', ');
137
+ return `Project "${projectQuery}" not found. Known projects: ${names}`;
138
+ }
139
+ return [
140
+ `# ${project.name}`,
141
+ '',
142
+ `**Path:** ${project.path}`,
143
+ `**Language:** ${project.language}`,
144
+ `**Tech Stack:** ${project.techStack.join(', ')}`,
145
+ `**Branch:** ${project.currentBranch}${project.isDirty ? ' (uncommitted changes)' : ''}`,
146
+ `**Last Activity:** ${new Date(project.lastActivity).toLocaleString()}`,
147
+ '',
148
+ '## Description',
149
+ project.description,
150
+ '',
151
+ '## Key Files',
152
+ `- CLAUDE.md: ${project.hasFiles.claudeMd ? '✓ exists' : '✗ missing'}`,
153
+ `- README.md: ${project.hasFiles.readme ? '✓ exists' : '✗ missing'}`,
154
+ '',
155
+ project.insights.length > 0
156
+ ? ['## Insights', ...project.insights.map((i) => `- **${i.title}:** ${i.description}`)].join('\n')
157
+ : '',
158
+ ].join('\n');
159
+ }
160
+ function handleSearchProjects(query) {
161
+ const context = loadContext();
162
+ if (!context) {
163
+ return 'No projects scanned yet. Run `claude-memory scan` first.';
164
+ }
165
+ const q = query.toLowerCase();
166
+ const matches = context.projects.filter((p) => p.name.toLowerCase().includes(q) ||
167
+ p.description.toLowerCase().includes(q) ||
168
+ p.techStack.some((t) => t.toLowerCase().includes(q)));
169
+ if (matches.length === 0) {
170
+ return `No projects match "${query}".`;
171
+ }
172
+ const lines = [`# Search Results for "${query}"`, ''];
173
+ for (const p of matches) {
174
+ lines.push(`- **${p.name}** - ${p.techStack.join(', ')}`);
175
+ lines.push(` ${p.description.slice(0, 100)}...`);
176
+ }
177
+ return lines.join('\n');
178
+ }
179
+ function handleRecordInsight(content, relatedProjects) {
180
+ const contextPath = join(MEMORY_DIR, 'context.json');
181
+ if (!existsSync(contextPath)) {
182
+ return 'No context file found. Run `claude-memory scan` first.';
183
+ }
184
+ try {
185
+ const context = JSON.parse(readFileSync(contextPath, 'utf-8'));
186
+ // Add the new insight
187
+ context.insights.push({
188
+ content,
189
+ relatedProjects: relatedProjects || [],
190
+ discoveredAt: new Date().toISOString(),
191
+ });
192
+ // Write back
193
+ writeFileSync(contextPath, JSON.stringify(context, null, 2), 'utf-8');
194
+ return `✅ Insight recorded: "${content.slice(0, 100)}..."${relatedProjects?.length ? ` (related to: ${relatedProjects.join(', ')})` : ''}`;
195
+ }
196
+ catch (err) {
197
+ return `Failed to record insight: ${err instanceof Error ? err.message : 'Unknown error'}`;
198
+ }
199
+ }
200
+ function handleGetProjectAnalysis(projectQuery) {
201
+ const context = loadContext();
202
+ if (!context) {
203
+ return 'No projects scanned yet. Run `claude-memory scan` first.';
204
+ }
205
+ const project = context.projects.find((p) => p.name.toLowerCase() === projectQuery.toLowerCase() ||
206
+ p.path.toLowerCase().includes(projectQuery.toLowerCase()));
207
+ if (!project) {
208
+ const names = context.projects.map((p) => p.name).join(', ');
209
+ return `Project "${projectQuery}" not found. Known projects: ${names}`;
210
+ }
211
+ const lines = [
212
+ '# Project Analysis for CLAUDE.md Generation',
213
+ '',
214
+ `## Basic Info`,
215
+ `- **Name:** ${project.name}`,
216
+ `- **Path:** ${project.path}`,
217
+ `- **Language:** ${project.language}`,
218
+ `- **Tech Stack:** ${project.techStack.join(', ')}`,
219
+ `- **Branch:** ${project.currentBranch}${project.isDirty ? ' (uncommitted changes)' : ''}`,
220
+ `- **Last Activity:** ${new Date(project.lastActivity).toLocaleString()}`,
221
+ '',
222
+ ];
223
+ // Directory structure
224
+ lines.push('## Directory Structure');
225
+ lines.push('```');
226
+ lines.push(getDirectoryStructure(project.path, 3));
227
+ lines.push('```');
228
+ lines.push('');
229
+ // README content
230
+ const readmePath = join(project.path, 'README.md');
231
+ if (existsSync(readmePath)) {
232
+ try {
233
+ const readme = readFileSync(readmePath, 'utf-8').slice(0, 3000);
234
+ lines.push('## README.md Content');
235
+ lines.push('```markdown');
236
+ lines.push(readme);
237
+ lines.push('```');
238
+ lines.push('');
239
+ }
240
+ catch {
241
+ // Skip if can't read
242
+ }
243
+ }
244
+ // Package.json info
245
+ const pkgPath = join(project.path, 'package.json');
246
+ if (existsSync(pkgPath)) {
247
+ try {
248
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
249
+ lines.push('## package.json');
250
+ lines.push(`- **Name:** ${pkg.name || 'unnamed'}`);
251
+ lines.push(`- **Description:** ${pkg.description || 'none'}`);
252
+ if (pkg.scripts) {
253
+ lines.push('- **Scripts:**');
254
+ for (const [name, cmd] of Object.entries(pkg.scripts)) {
255
+ lines.push(` - \`${name}\`: ${cmd}`);
256
+ }
257
+ }
258
+ if (pkg.dependencies) {
259
+ lines.push(`- **Dependencies:** ${Object.keys(pkg.dependencies).join(', ')}`);
260
+ }
261
+ lines.push('');
262
+ }
263
+ catch {
264
+ // Skip if can't read
265
+ }
266
+ }
267
+ // Key files
268
+ lines.push('## Key Entry Files');
269
+ const keyPatterns = [
270
+ 'src/index.ts', 'src/index.js', 'src/main.ts', 'src/main.js',
271
+ 'src/app.ts', 'src/app.js', 'src/cli.ts', 'src/cli.js',
272
+ 'index.ts', 'index.js', 'main.ts', 'main.js',
273
+ 'main.py', 'app.py', '__main__.py',
274
+ 'src/lib.rs', 'src/main.rs', 'main.go',
275
+ ];
276
+ for (const pattern of keyPatterns) {
277
+ if (existsSync(join(project.path, pattern))) {
278
+ lines.push(`- ${pattern}`);
279
+ }
280
+ }
281
+ lines.push('');
282
+ lines.push('## Instructions');
283
+ lines.push('Based on the above analysis, please generate a CLAUDE.md file that includes:');
284
+ lines.push('1. Project title and one-line description');
285
+ lines.push('2. What the project does (2-3 sentences)');
286
+ lines.push('3. Tech stack overview');
287
+ lines.push('4. Project structure explanation');
288
+ lines.push('5. How to run/build');
289
+ lines.push('6. Any important context for future Claude sessions');
290
+ return lines.join('\n');
291
+ }
292
+ function getDirectoryStructure(dir, maxDepth, prefix = '', depth = 0) {
293
+ if (depth >= maxDepth)
294
+ return '';
295
+ const ignore = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'target', '.next', '.nuxt', 'coverage', '.cache', '.venv', 'venv'];
296
+ let result = '';
297
+ try {
298
+ const entries = readdirSync(dir).filter(e => !ignore.includes(e) && !e.startsWith('.'));
299
+ entries.sort((a, b) => {
300
+ const aIsDir = statSync(join(dir, a)).isDirectory();
301
+ const bIsDir = statSync(join(dir, b)).isDirectory();
302
+ if (aIsDir && !bIsDir)
303
+ return -1;
304
+ if (!aIsDir && bIsDir)
305
+ return 1;
306
+ return a.localeCompare(b);
307
+ });
308
+ for (let i = 0; i < entries.length; i++) {
309
+ const entry = entries[i];
310
+ const fullPath = join(dir, entry);
311
+ const isLast = i === entries.length - 1;
312
+ const connector = isLast ? '└── ' : '├── ';
313
+ const newPrefix = prefix + (isLast ? ' ' : '│ ');
314
+ try {
315
+ const stat = statSync(fullPath);
316
+ if (stat.isDirectory()) {
317
+ result += `${prefix}${connector}${entry}/\n`;
318
+ result += getDirectoryStructure(fullPath, maxDepth, newPrefix, depth + 1);
319
+ }
320
+ else {
321
+ result += `${prefix}${connector}${entry}\n`;
322
+ }
323
+ }
324
+ catch {
325
+ // Skip inaccessible
326
+ }
327
+ }
328
+ }
329
+ catch {
330
+ // Skip inaccessible
331
+ }
332
+ return result;
333
+ }
334
+ async function main() {
335
+ const server = new Server({
336
+ name: 'claude-memory',
337
+ version: '0.1.0',
338
+ }, {
339
+ capabilities: {
340
+ tools: {},
341
+ },
342
+ });
343
+ // List tools
344
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
345
+ tools,
346
+ }));
347
+ // Handle tool calls
348
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
349
+ const { name, arguments: args } = request.params;
350
+ try {
351
+ let result;
352
+ switch (name) {
353
+ case 'get_global_context':
354
+ result = handleGetGlobalContext();
355
+ break;
356
+ case 'get_project_summary':
357
+ result = handleGetProjectSummary(args.project);
358
+ break;
359
+ case 'search_projects':
360
+ result = handleSearchProjects(args.query);
361
+ break;
362
+ case 'record_insight':
363
+ result = handleRecordInsight(args.content, args.relatedProjects);
364
+ break;
365
+ case 'get_project_analysis':
366
+ result = handleGetProjectAnalysis(args.project);
367
+ break;
368
+ default:
369
+ throw new Error(`Unknown tool: ${name}`);
370
+ }
371
+ return {
372
+ content: [{ type: 'text', text: result }],
373
+ };
374
+ }
375
+ catch (error) {
376
+ return {
377
+ content: [
378
+ {
379
+ type: 'text',
380
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
381
+ },
382
+ ],
383
+ isError: true,
384
+ };
385
+ }
386
+ });
387
+ const transport = new StdioServerTransport();
388
+ await server.connect(transport);
389
+ console.error('Claude Memory MCP server running');
390
+ }
391
+ main().catch(console.error);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Context Generator - Produces markdown context files
3
+ */
4
+ import { ProjectInfo, GlobalContext } from '../types/index.js';
5
+ /**
6
+ * Generate global context from scanned projects
7
+ */
8
+ export declare function generateGlobalContext(projects: ProjectInfo[]): GlobalContext;
9
+ /**
10
+ * Write global context to ~/.claude-memory/
11
+ */
12
+ export declare function writeGlobalContext(context: GlobalContext): void;
13
+ /**
14
+ * Get the memory directory path
15
+ */
16
+ export declare function getMemoryDir(): string;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Context Generator - Produces markdown context files
3
+ */
4
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { homedir } from 'os';
7
+ const MEMORY_DIR = join(homedir(), '.claude-memory');
8
+ /**
9
+ * Generate global context from scanned projects
10
+ */
11
+ export function generateGlobalContext(projects) {
12
+ return {
13
+ lastUpdated: new Date().toISOString(),
14
+ projects,
15
+ insights: [], // TODO: Cross-project analysis
16
+ userPatterns: [], // TODO: Pattern detection
17
+ };
18
+ }
19
+ /**
20
+ * Write global context to ~/.claude-memory/
21
+ */
22
+ export function writeGlobalContext(context) {
23
+ // Ensure directory exists
24
+ if (!existsSync(MEMORY_DIR)) {
25
+ mkdirSync(MEMORY_DIR, { recursive: true });
26
+ }
27
+ // Write JSON for MCP server
28
+ const jsonPath = join(MEMORY_DIR, 'context.json');
29
+ writeFileSync(jsonPath, JSON.stringify(context, null, 2));
30
+ // Write markdown for human readability
31
+ const mdPath = join(MEMORY_DIR, 'global-context.md');
32
+ writeFileSync(mdPath, formatGlobalContextMarkdown(context));
33
+ console.log(`\n📝 Written to:`);
34
+ console.log(` ${jsonPath}`);
35
+ console.log(` ${mdPath}`);
36
+ }
37
+ /**
38
+ * Format global context as markdown
39
+ */
40
+ function formatGlobalContextMarkdown(context) {
41
+ const lines = [
42
+ '# Claude Global Memory',
43
+ '',
44
+ `> Last updated: ${new Date(context.lastUpdated).toLocaleString()}`,
45
+ '',
46
+ '## Your Projects',
47
+ '',
48
+ ];
49
+ // Sort by last activity (most recent first)
50
+ const sorted = [...context.projects].sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
51
+ for (const project of sorted) {
52
+ const activityDate = new Date(project.lastActivity).toLocaleDateString();
53
+ const dirty = project.isDirty ? ' ⚠️ uncommitted changes' : '';
54
+ lines.push(`### ${project.name}`);
55
+ lines.push('');
56
+ lines.push(`**Path:** \`${project.path}\``);
57
+ lines.push(`**Stack:** ${project.techStack.join(', ') || 'Unknown'}`);
58
+ lines.push(`**Branch:** ${project.currentBranch}${dirty}`);
59
+ lines.push(`**Last Activity:** ${activityDate}`);
60
+ lines.push('');
61
+ lines.push(`> ${project.description}`);
62
+ lines.push('');
63
+ // Key files
64
+ const keyFiles = [];
65
+ if (project.hasFiles.claudeMd)
66
+ keyFiles.push('CLAUDE.md');
67
+ if (project.hasFiles.readme)
68
+ keyFiles.push('README.md');
69
+ if (keyFiles.length > 0) {
70
+ lines.push(`📄 Has: ${keyFiles.join(', ')}`);
71
+ lines.push('');
72
+ }
73
+ lines.push('---');
74
+ lines.push('');
75
+ }
76
+ // Summary stats
77
+ lines.push('## Summary');
78
+ lines.push('');
79
+ lines.push(`- **Total Projects:** ${context.projects.length}`);
80
+ const byLanguage = context.projects.reduce((acc, p) => {
81
+ acc[p.language] = (acc[p.language] || 0) + 1;
82
+ return acc;
83
+ }, {});
84
+ lines.push(`- **Languages:** ${Object.entries(byLanguage).map(([l, c]) => `${l} (${c})`).join(', ')}`);
85
+ const withClaudeMd = context.projects.filter((p) => p.hasFiles.claudeMd).length;
86
+ lines.push(`- **With CLAUDE.md:** ${withClaudeMd}/${context.projects.length}`);
87
+ const dirty = context.projects.filter((p) => p.isDirty).length;
88
+ if (dirty > 0) {
89
+ lines.push(`- **⚠️ Uncommitted Changes:** ${dirty} projects`);
90
+ }
91
+ return lines.join('\n');
92
+ }
93
+ /**
94
+ * Get the memory directory path
95
+ */
96
+ export function getMemoryDir() {
97
+ return MEMORY_DIR;
98
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Project Scanner - Discovers and analyzes projects
3
+ */
4
+ import { ProjectInfo, ScanOptions } from '../types/index.js';
5
+ /**
6
+ * Scan a directory for projects and extract information
7
+ */
8
+ export declare function scanProjects(options?: Partial<ScanOptions>): Promise<ProjectInfo[]>;