@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.
- package/.claude/settings.local.json +10 -0
- package/README.md +108 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +139 -0
- package/dist/generator/claudeMdGenerator.d.ts +20 -0
- package/dist/generator/claudeMdGenerator.js +202 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +391 -0
- package/dist/scanner/contextGenerator.d.ts +16 -0
- package/dist/scanner/contextGenerator.js +98 -0
- package/dist/scanner/projectScanner.d.ts +8 -0
- package/dist/scanner/projectScanner.js +319 -0
- package/dist/types/index.d.ts +87 -0
- package/dist/types/index.js +10 -0
- package/package.json +37 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Scanner - Discovers and analyzes projects
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
5
|
+
import { join, basename } from 'path';
|
|
6
|
+
import { simpleGit } from 'simple-git';
|
|
7
|
+
import { DEFAULT_SCAN_OPTIONS } from '../types/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Scan a directory for projects and extract information
|
|
10
|
+
*/
|
|
11
|
+
export async function scanProjects(options = {}) {
|
|
12
|
+
const opts = { ...DEFAULT_SCAN_OPTIONS, ...options };
|
|
13
|
+
const projects = [];
|
|
14
|
+
console.log(`š Scanning ${opts.rootDir} for projects...`);
|
|
15
|
+
const candidates = findProjectRoots(opts.rootDir, opts.maxDepth, opts.ignore);
|
|
16
|
+
for (const projectPath of candidates) {
|
|
17
|
+
try {
|
|
18
|
+
const info = await analyzeProject(projectPath);
|
|
19
|
+
if (info) {
|
|
20
|
+
projects.push(info);
|
|
21
|
+
console.log(` ā Found: ${info.name} (${info.language})`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.warn(` ā Failed to analyze: ${projectPath}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
console.log(`\nš Found ${projects.length} projects`);
|
|
29
|
+
return projects;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Find directories that look like project roots
|
|
33
|
+
*/
|
|
34
|
+
function findProjectRoots(rootDir, maxDepth, ignore) {
|
|
35
|
+
const roots = [];
|
|
36
|
+
function walk(dir, depth) {
|
|
37
|
+
if (depth > maxDepth)
|
|
38
|
+
return;
|
|
39
|
+
try {
|
|
40
|
+
const entries = readdirSync(dir);
|
|
41
|
+
// Check if this is a project root
|
|
42
|
+
const isProject = entries.includes('package.json') ||
|
|
43
|
+
entries.includes('Cargo.toml') ||
|
|
44
|
+
entries.includes('pyproject.toml') ||
|
|
45
|
+
entries.includes('go.mod') ||
|
|
46
|
+
entries.includes('.git');
|
|
47
|
+
if (isProject) {
|
|
48
|
+
roots.push(dir);
|
|
49
|
+
return; // Don't recurse into project subdirectories
|
|
50
|
+
}
|
|
51
|
+
// Recurse into subdirectories
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (ignore.includes(entry))
|
|
54
|
+
continue;
|
|
55
|
+
const fullPath = join(dir, entry);
|
|
56
|
+
try {
|
|
57
|
+
if (statSync(fullPath).isDirectory()) {
|
|
58
|
+
walk(fullPath, depth + 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Skip inaccessible directories
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Skip inaccessible directories
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(rootDir, 0);
|
|
71
|
+
return roots;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Analyze a single project
|
|
75
|
+
*/
|
|
76
|
+
async function analyzeProject(projectPath) {
|
|
77
|
+
const name = detectProjectName(projectPath);
|
|
78
|
+
const description = detectDescription(projectPath);
|
|
79
|
+
const { language, techStack } = detectTechStack(projectPath);
|
|
80
|
+
// Git info
|
|
81
|
+
let lastActivity = new Date().toISOString();
|
|
82
|
+
let currentBranch = 'unknown';
|
|
83
|
+
let isDirty = false;
|
|
84
|
+
if (existsSync(join(projectPath, '.git'))) {
|
|
85
|
+
try {
|
|
86
|
+
const git = simpleGit(projectPath);
|
|
87
|
+
const log = await git.log({ maxCount: 1 });
|
|
88
|
+
if (log.latest) {
|
|
89
|
+
lastActivity = log.latest.date;
|
|
90
|
+
}
|
|
91
|
+
const status = await git.status();
|
|
92
|
+
currentBranch = status.current || 'unknown';
|
|
93
|
+
isDirty = !status.isClean();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Git operations failed, use defaults
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Extract insights from CLAUDE.md if it exists
|
|
100
|
+
const insights = extractInsightsFromClaudeMd(projectPath);
|
|
101
|
+
return {
|
|
102
|
+
path: projectPath,
|
|
103
|
+
name,
|
|
104
|
+
description,
|
|
105
|
+
techStack,
|
|
106
|
+
language,
|
|
107
|
+
lastActivity,
|
|
108
|
+
currentBranch,
|
|
109
|
+
isDirty,
|
|
110
|
+
hasFiles: {
|
|
111
|
+
claudeMd: existsSync(join(projectPath, 'CLAUDE.md')),
|
|
112
|
+
readme: existsSync(join(projectPath, 'README.md')),
|
|
113
|
+
packageJson: existsSync(join(projectPath, 'package.json')),
|
|
114
|
+
gitignore: existsSync(join(projectPath, '.gitignore')),
|
|
115
|
+
},
|
|
116
|
+
insights,
|
|
117
|
+
lastScanned: new Date().toISOString(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Detect project name from various sources
|
|
122
|
+
*/
|
|
123
|
+
function detectProjectName(projectPath) {
|
|
124
|
+
// Try package.json
|
|
125
|
+
const pkgPath = join(projectPath, 'package.json');
|
|
126
|
+
if (existsSync(pkgPath)) {
|
|
127
|
+
try {
|
|
128
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
129
|
+
if (pkg.name)
|
|
130
|
+
return pkg.name;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Invalid JSON
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Try Cargo.toml
|
|
137
|
+
const cargoPath = join(projectPath, 'Cargo.toml');
|
|
138
|
+
if (existsSync(cargoPath)) {
|
|
139
|
+
try {
|
|
140
|
+
const cargo = readFileSync(cargoPath, 'utf-8');
|
|
141
|
+
const match = cargo.match(/name\s*=\s*"([^"]+)"/);
|
|
142
|
+
if (match)
|
|
143
|
+
return match[1];
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Read failed
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Fall back to folder name
|
|
150
|
+
return basename(projectPath);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Detect project description
|
|
154
|
+
*/
|
|
155
|
+
function detectDescription(projectPath) {
|
|
156
|
+
// Try package.json
|
|
157
|
+
const pkgPath = join(projectPath, 'package.json');
|
|
158
|
+
if (existsSync(pkgPath)) {
|
|
159
|
+
try {
|
|
160
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
161
|
+
if (pkg.description)
|
|
162
|
+
return pkg.description;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Invalid JSON
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Try README first line
|
|
169
|
+
const readmePath = join(projectPath, 'README.md');
|
|
170
|
+
if (existsSync(readmePath)) {
|
|
171
|
+
try {
|
|
172
|
+
const readme = readFileSync(readmePath, 'utf-8');
|
|
173
|
+
const lines = readme.split('\n').filter((l) => l.trim() && !l.startsWith('#'));
|
|
174
|
+
if (lines[0])
|
|
175
|
+
return lines[0].slice(0, 200);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Read failed
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Try CLAUDE.md first paragraph
|
|
182
|
+
const claudePath = join(projectPath, 'CLAUDE.md');
|
|
183
|
+
if (existsSync(claudePath)) {
|
|
184
|
+
try {
|
|
185
|
+
const claude = readFileSync(claudePath, 'utf-8');
|
|
186
|
+
const match = claude.match(/##\s*Project Overview\s*\n+([^\n]+)/i);
|
|
187
|
+
if (match)
|
|
188
|
+
return match[1].slice(0, 200);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Read failed
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return 'No description available';
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Detect tech stack and primary language
|
|
198
|
+
*/
|
|
199
|
+
function detectTechStack(projectPath) {
|
|
200
|
+
const techStack = [];
|
|
201
|
+
let language = 'other';
|
|
202
|
+
// Check package.json
|
|
203
|
+
const pkgPath = join(projectPath, 'package.json');
|
|
204
|
+
if (existsSync(pkgPath)) {
|
|
205
|
+
try {
|
|
206
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
207
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
208
|
+
// Detect TypeScript
|
|
209
|
+
if (deps.typescript || existsSync(join(projectPath, 'tsconfig.json'))) {
|
|
210
|
+
language = 'typescript';
|
|
211
|
+
techStack.push('TypeScript');
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
language = 'javascript';
|
|
215
|
+
techStack.push('JavaScript');
|
|
216
|
+
}
|
|
217
|
+
// Detect frameworks
|
|
218
|
+
if (deps.react)
|
|
219
|
+
techStack.push('React');
|
|
220
|
+
if (deps.vue)
|
|
221
|
+
techStack.push('Vue');
|
|
222
|
+
if (deps.next)
|
|
223
|
+
techStack.push('Next.js');
|
|
224
|
+
if (deps.express)
|
|
225
|
+
techStack.push('Express');
|
|
226
|
+
if (deps.fastify)
|
|
227
|
+
techStack.push('Fastify');
|
|
228
|
+
if (deps['@anthropic-ai/sdk'])
|
|
229
|
+
techStack.push('Claude SDK');
|
|
230
|
+
if (deps['@modelcontextprotocol/sdk'])
|
|
231
|
+
techStack.push('MCP');
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Invalid JSON
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Check Cargo.toml
|
|
238
|
+
if (existsSync(join(projectPath, 'Cargo.toml'))) {
|
|
239
|
+
language = 'rust';
|
|
240
|
+
techStack.push('Rust');
|
|
241
|
+
}
|
|
242
|
+
// Check pyproject.toml or requirements.txt
|
|
243
|
+
if (existsSync(join(projectPath, 'pyproject.toml')) ||
|
|
244
|
+
existsSync(join(projectPath, 'requirements.txt'))) {
|
|
245
|
+
language = 'python';
|
|
246
|
+
techStack.push('Python');
|
|
247
|
+
}
|
|
248
|
+
// Check go.mod
|
|
249
|
+
if (existsSync(join(projectPath, 'go.mod'))) {
|
|
250
|
+
language = 'go';
|
|
251
|
+
techStack.push('Go');
|
|
252
|
+
}
|
|
253
|
+
return { language, techStack };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Extract insights from CLAUDE.md file
|
|
257
|
+
* Looks for patterns like:
|
|
258
|
+
* - ## Session Context / ## Current State sections
|
|
259
|
+
* - Key architectural decisions
|
|
260
|
+
* - TODOs and next steps
|
|
261
|
+
*/
|
|
262
|
+
function extractInsightsFromClaudeMd(projectPath) {
|
|
263
|
+
const claudePath = join(projectPath, 'CLAUDE.md');
|
|
264
|
+
if (!existsSync(claudePath)) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
const insights = [];
|
|
268
|
+
try {
|
|
269
|
+
const content = readFileSync(claudePath, 'utf-8');
|
|
270
|
+
// Extract "Session Context" or "Current State" sections
|
|
271
|
+
const sessionMatch = content.match(/##\s*(Session Context|Current State|Where we left off)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
272
|
+
if (sessionMatch) {
|
|
273
|
+
insights.push({
|
|
274
|
+
type: 'notable',
|
|
275
|
+
title: 'Session Context',
|
|
276
|
+
description: sessionMatch[2].trim().slice(0, 500),
|
|
277
|
+
source: 'CLAUDE.md',
|
|
278
|
+
discoveredAt: new Date().toISOString(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// Extract "What's Been Built" or similar sections
|
|
282
|
+
const builtMatch = content.match(/##\s*(What's Been Built|Features|Implemented)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
283
|
+
if (builtMatch) {
|
|
284
|
+
insights.push({
|
|
285
|
+
type: 'architecture',
|
|
286
|
+
title: 'What\'s Built',
|
|
287
|
+
description: builtMatch[2].trim().slice(0, 500),
|
|
288
|
+
source: 'CLAUDE.md',
|
|
289
|
+
discoveredAt: new Date().toISOString(),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
// Extract "Next Steps" or "TODO" sections
|
|
293
|
+
const todoMatch = content.match(/##\s*(Next Steps|TODO|Roadmap|Immediate)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
294
|
+
if (todoMatch) {
|
|
295
|
+
insights.push({
|
|
296
|
+
type: 'todo',
|
|
297
|
+
title: 'Next Steps',
|
|
298
|
+
description: todoMatch[2].trim().slice(0, 500),
|
|
299
|
+
source: 'CLAUDE.md',
|
|
300
|
+
discoveredAt: new Date().toISOString(),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// Extract architecture/tech decisions
|
|
304
|
+
const archMatch = content.match(/##\s*(Architecture|Tech Stack|Design)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
305
|
+
if (archMatch) {
|
|
306
|
+
insights.push({
|
|
307
|
+
type: 'architecture',
|
|
308
|
+
title: 'Architecture',
|
|
309
|
+
description: archMatch[2].trim().slice(0, 500),
|
|
310
|
+
source: 'CLAUDE.md',
|
|
311
|
+
discoveredAt: new Date().toISOString(),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Failed to read or parse
|
|
317
|
+
}
|
|
318
|
+
return insights;
|
|
319
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for Claude Memory
|
|
3
|
+
*/
|
|
4
|
+
export interface ProjectInfo {
|
|
5
|
+
/** Absolute path to project root */
|
|
6
|
+
path: string;
|
|
7
|
+
/** Project name (from package.json, Cargo.toml, or folder name) */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Short description (from package.json, README first line, etc.) */
|
|
10
|
+
description: string;
|
|
11
|
+
/** Detected tech stack */
|
|
12
|
+
techStack: string[];
|
|
13
|
+
/** Primary language */
|
|
14
|
+
language: 'typescript' | 'javascript' | 'python' | 'rust' | 'go' | 'other';
|
|
15
|
+
/** Last git commit date */
|
|
16
|
+
lastActivity: string;
|
|
17
|
+
/** Current git branch */
|
|
18
|
+
currentBranch: string;
|
|
19
|
+
/** Has uncommitted changes */
|
|
20
|
+
isDirty: boolean;
|
|
21
|
+
/** Key files that exist */
|
|
22
|
+
hasFiles: {
|
|
23
|
+
claudeMd: boolean;
|
|
24
|
+
readme: boolean;
|
|
25
|
+
packageJson: boolean;
|
|
26
|
+
gitignore: boolean;
|
|
27
|
+
};
|
|
28
|
+
/** Extracted insights (patterns, conventions, notable things) */
|
|
29
|
+
insights: ProjectInsight[];
|
|
30
|
+
/** When this project was last scanned */
|
|
31
|
+
lastScanned: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ProjectInsight {
|
|
34
|
+
/** What kind of insight */
|
|
35
|
+
type: 'pattern' | 'convention' | 'architecture' | 'notable' | 'todo';
|
|
36
|
+
/** Short title */
|
|
37
|
+
title: string;
|
|
38
|
+
/** Description */
|
|
39
|
+
description: string;
|
|
40
|
+
/** Source file (if applicable) */
|
|
41
|
+
source?: string;
|
|
42
|
+
/** When discovered */
|
|
43
|
+
discoveredAt: string;
|
|
44
|
+
}
|
|
45
|
+
export interface GlobalContext {
|
|
46
|
+
/** When the global context was last updated */
|
|
47
|
+
lastUpdated: string;
|
|
48
|
+
/** All scanned projects */
|
|
49
|
+
projects: ProjectInfo[];
|
|
50
|
+
/** Cross-project insights */
|
|
51
|
+
insights: GlobalInsight[];
|
|
52
|
+
/** User preferences/patterns observed */
|
|
53
|
+
userPatterns: UserPattern[];
|
|
54
|
+
}
|
|
55
|
+
export interface GlobalInsight {
|
|
56
|
+
/** Insight content */
|
|
57
|
+
content: string;
|
|
58
|
+
/** Which projects this relates to */
|
|
59
|
+
relatedProjects: string[];
|
|
60
|
+
/** When discovered */
|
|
61
|
+
discoveredAt: string;
|
|
62
|
+
}
|
|
63
|
+
export interface UserPattern {
|
|
64
|
+
/** Pattern name */
|
|
65
|
+
name: string;
|
|
66
|
+
/** Description */
|
|
67
|
+
description: string;
|
|
68
|
+
/** Examples from projects */
|
|
69
|
+
examples: Array<{
|
|
70
|
+
project: string;
|
|
71
|
+
file: string;
|
|
72
|
+
snippet?: string;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
export interface ScanOptions {
|
|
76
|
+
/** Root directory to scan (default: ~/Projects) */
|
|
77
|
+
rootDir: string;
|
|
78
|
+
/** Max depth to search for projects */
|
|
79
|
+
maxDepth: number;
|
|
80
|
+
/** Patterns to ignore */
|
|
81
|
+
ignore: string[];
|
|
82
|
+
/** Whether to generate CLAUDE.md files */
|
|
83
|
+
generateClaudeMd: boolean;
|
|
84
|
+
/** Whether to overwrite existing CLAUDE.md */
|
|
85
|
+
overwriteClaudeMd: boolean;
|
|
86
|
+
}
|
|
87
|
+
export declare const DEFAULT_SCAN_OPTIONS: ScanOptions;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for Claude Memory
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_SCAN_OPTIONS = {
|
|
5
|
+
rootDir: process.env.HOME ? `${process.env.HOME}/Project` : './projects',
|
|
6
|
+
maxDepth: 2,
|
|
7
|
+
ignore: ['node_modules', '.git', 'dist', 'build', '__pycache__', 'target'],
|
|
8
|
+
generateClaudeMd: false,
|
|
9
|
+
overwriteClaudeMd: false,
|
|
10
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contextmirror/claude-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cross-project memory for Claude Code - know about all your projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"claude-memory": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"scan": "node dist/cli.js scan",
|
|
14
|
+
"mcp": "node dist/mcp/server.js",
|
|
15
|
+
"test": "vitest"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude",
|
|
19
|
+
"mcp",
|
|
20
|
+
"memory",
|
|
21
|
+
"context",
|
|
22
|
+
"ai"
|
|
23
|
+
],
|
|
24
|
+
"author": "Nathan",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
+
"commander": "^12.1.0",
|
|
29
|
+
"glob": "^11.0.0",
|
|
30
|
+
"simple-git": "^3.27.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.10.2",
|
|
34
|
+
"typescript": "^5.7.2",
|
|
35
|
+
"vitest": "^2.1.8"
|
|
36
|
+
}
|
|
37
|
+
}
|